pax_global_header00006660000000000000000000000064147775730710014534gustar00rootroot0000000000000052 comment=95e00e4bee51a4f65ced4723b362fae288c40f59 uqfoundation-pox-95e00e4/000077500000000000000000000000001477757307100154115ustar00rootroot00000000000000uqfoundation-pox-95e00e4/.codecov.yml000066400000000000000000000014751477757307100176430ustar00rootroot00000000000000comment: false coverage: status: project: default: # Commits pushed to master should not make the overall # project coverage decrease by more than 1%: target: auto threshold: 1% patch: default: # Be tolerant on slight code coverage diff on PRs to limit # noisy red coverage status on github PRs. # Note The coverage stats are still uploaded # to codecov so that PR reviewers can see uncovered lines # in the github diff if they install the codecov browser # extension: # https://github.com/codecov/browser-extension target: auto threshold: 1% fixes: # reduces pip-installed path to git root and # remove dist-name from setup-installed path - "*/site-packages/::" - "*/site-packages/pox-*::" uqfoundation-pox-95e00e4/.coveragerc000066400000000000000000000010341477757307100175300ustar00rootroot00000000000000[run] # source = pox include = */pox/* omit = */tests/* */info.py branch = true # timid = true # parallel = true # and need to 'combine' data files # concurrency = multiprocessing # thread # data_file = $TRAVIS_BUILD_DIR/.coverage # debug = trace [paths] source = pox */site-packages/pox */site-packages/pox-*/pox [report] include = */pox/* exclude_lines = pragma: no cover raise NotImplementedError if __name__ == .__main__.: # show_missing = true ignore_errors = true # pragma: no branch # noqa uqfoundation-pox-95e00e4/.gitignore000066400000000000000000000000401477757307100173730ustar00rootroot00000000000000.tox/ .cache/ *.egg-info/ *.pyc uqfoundation-pox-95e00e4/.readthedocs.yml000066400000000000000000000005151477757307100205000ustar00rootroot00000000000000# readthedocs configuration file # see https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 # configure sphinx: configuration: docs/source/conf.py # build build: os: ubuntu-22.04 tools: python: "3.10" # install python: install: - method: pip path: . - requirements: docs/requirements.txt uqfoundation-pox-95e00e4/.travis.yml000066400000000000000000000027061477757307100175270ustar00rootroot00000000000000dist: jammy language: python matrix: include: - python: '3.8' env: - python: '3.9' env: - COVERAGE="true" - python: '3.10' env: - python: '3.11' env: - python: '3.12' env: - python: '3.13' env: - python: '3.14-dev' env: - python: 'pypy3.8-7.3.9' # at 7.3.11 env: - python: 'pypy3.9-7.3.9' # at 7.3.16 env: - python: 'pypy3.10-7.3.19' env: - python: 'pypy3.11-7.3.19' env: allow_failures: - python: '3.14-dev' # CI missing - python: 'pypy3.10-7.3.19' # CI missing - python: 'pypy3.11-7.3.19' # CI missing fast_finish: true cache: pip: true before_install: - set -e # fail on any error - if [[ $COVERAGE == "true" ]]; then pip install coverage; fi install: - python -m pip install . script: - for test in pox/tests/__init__.py; do echo $test ; if [[ $COVERAGE == "true" ]]; then coverage run -a $test > /dev/null; else python $test > /dev/null; fi ; done - for test in pox/tests/test_*.py; do echo $test ; if [[ $COVERAGE == "true" ]]; then coverage run -a $test > /dev/null; else python $test > /dev/null; fi ; done after_success: - if [[ $COVERAGE == "true" ]]; then bash <(curl -s https://codecov.io/bash); else echo ''; fi - if [[ $COVERAGE == "true" ]]; then coverage report; fi uqfoundation-pox-95e00e4/LICENSE000066400000000000000000000033761477757307100164270ustar00rootroot00000000000000Copyright (c) 2004-2016 California Institute of Technology. Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. All rights reserved. This software is available subject to the conditions and terms laid out below. By downloading and using this software you are agreeing to the following conditions. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the names of the copyright holders nor the names of any of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. uqfoundation-pox-95e00e4/MANIFEST.in000066400000000000000000000003331477757307100171460ustar00rootroot00000000000000include LICENSE include README* include MANIFEST.in include pyproject.toml include tox.ini include version.py include scripts/* include tools/* recursive-include docs * include .* prune .git prune .coverage prune .eggs uqfoundation-pox-95e00e4/README.md000066400000000000000000000165421477757307100167000ustar00rootroot00000000000000pox === utilities for filesystem exploration and automated builds About Pox --------- ``pox`` provides a collection of utilities for navigating and manipulating filesystems. This module is designed to facilitate some of the low level operating system interactions that are useful when exploring a filesystem on a remote host, where queries such as *"what is the root of the filesystem?"*, *"what is the user's name?"*, and *"what login shell is preferred?"* become essential in allowing a remote user to function as if they were logged in locally. While ``pox`` is in the same vein of both the ``os`` and ``shutil`` builtin modules, the majority of its functionality is unique and compliments these two modules. ``pox`` provides Python equivalents of several unix shell commands such as ``which`` and ``find``. These commands allow automated discovery of what has been installed on an operating system, and where the essential tools are located. This capability is useful not only for exploring remote hosts, but also locally as a helper utility for automated build and installation. Several high-level operations on files and filesystems are also provided. Examples of which are: finding the location of an installed Python package, determining if and where the source code resides on the filesystem, and determining what version the installed package is. ``pox`` also provides utilities to enable the abstraction of commands sent to a remote filesystem. In conjunction with a registry of environment variables and installed utilites, ``pox`` enables the user to interact with a remote filesystem as if they were logged in locally. ``pox`` is part of ``pathos``, a Python framework for heterogeneous computing. ``pox`` is in active development, so any user feedback, bug reports, comments, or suggestions are highly appreciated. A list of issues is located at https://github.com/uqfoundation/pox/issues, with a legacy list maintained at https://uqfoundation.github.io/project/pathos/query. Major Features -------------- ``pox`` provides utilities for discovering the user's environment: * return the user's name, current shell, and path to user's home directory * strip duplicate entries from the user's ``$PATH`` * lookup and expand environment variables from ``${VAR}`` to ``value`` ``pox`` also provides utilities for filesystem exploration and manipulation: * discover the path to a file, exectuable, directory, or symbolic link * discover the path to an installed package * parse operating system commands for remote shell invocation * convert text files to platform-specific formatting Current Release [![Downloads](https://static.pepy.tech/personalized-badge/pox?period=total&units=international_system&left_color=grey&right_color=blue&left_text=pypi%20downloads)](https://pepy.tech/project/pox) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pox?color=blue&label=conda%20downloads)](https://anaconda.org/conda-forge/pox) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-get%20help-black.svg)](https://stackoverflow.com/questions/tagged/pathos) --------------- The latest released version of ``pox`` is available from: https://pypi.org/project/pox ``pox`` is distributed under a 3-clause BSD license. Development Version [![Support](https://img.shields.io/badge/support-the%20UQ%20Foundation-purple.svg?style=flat&colorA=grey&colorB=purple)](http://www.uqfoundation.org/pages/donate.html) [![Documentation Status](https://readthedocs.org/projects/pox/badge/?version=latest)](https://pox.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://app.travis-ci.com/uqfoundation/pox.svg?label=build&logo=travis&branch=master)](https://app.travis-ci.com/github/uqfoundation/pox) [![codecov](https://codecov.io/gh/uqfoundation/pox/branch/master/graph/badge.svg)](https://codecov.io/gh/uqfoundation/pox) ------------------- You can get the latest development version with all the shiny new features at: https://github.com/uqfoundation If you have a new contribution, please submit a pull request. Installation ------------ ``pox`` can be installed with ``pip``:: $ pip install pox Requirements ------------ ``pox`` requires: * ``python`` (or ``pypy``), **>=3.8** * ``setuptools``, **>=42** Basic Usage ----------- ``pox`` includes some basic utilities to connect to and automate exploration on local and remote filesystems. There are some basic functions to discover important locations:: >>> import pox >>> pox.homedir() '/Users/mmckerns' >>> pox.rootdir() '/' or, you can interact with local and global environment variables:: >>> local = {'DEV':'${HOME}/dev', 'FOO_VERSION':'0.1', 'BAR_VERSION':'1.0'} >>> pox.getvars('${DEV}/lib/foo-${FOO_VERSION}', local) {'DEV': '${HOME}/dev', 'FOO_VERSION': '0.1'} >>> pox.expandvars('${DEV}/lib/foo-${FOO_VERSION}', local) '${HOME}/dev/lib/foo-0.1' >>> pox.expandvars('${HOME}/dev/lib/foo-0.1') '/Users/mmckerns/dev/lib/foo-0.1' >>> pox.env('HOME') {'HOME': '/Users/mmckerns'} and perform some basic search functions:: >>> pox.find('python3.9', recurse=5, root='/opt') ['/opt/local/bin/python3.9'] >>> pox.which('python3.9') '/opt/local/bin/python3.9' ``pox`` also has a specialized `which` command just for Python:: >>> pox.which_python() '/opt/local/bin/python3.9' >>> pox.which_python(lazy=True, version=True) '`which python3.9`' Any of the ``pox`` functions can be launched from the command line, which facilitates executing commands across parallel and distributed pipes (such as `pathos.connection.Pipe` and `pathos.secure.connection.Pipe`):: >>> import pathos >>> p = pathos.connection.Pipe() >>> p(command="python -m pox 'which_python()'") >>> p.launch() >>> print(p.response()) '/usr/bin/python' >>> p.kill() The functions in ``pox`` that help make interactions with filesystems and environment varialbles programmatic and abstract become especially relevant when trying to execute complex commands remotely. More Information ---------------- Probably the best way to get started is to look at the documentation at http://pox.rtfd.io. Also see ``pox.tests`` for a set of scripts that demonstrate how ``pox`` can be used to interact with the operating system. You can run the test suite with ``python -m pox.tests``. All ``pox`` utilities can be launched directly from an operating system terminal, using the ``pox`` script (or with ``python -m pox``). The source code is also generally well documented, so further questions may be resolved by inspecting the code itself. Please feel free to submit a ticket on github, or ask a question on stackoverflow (**@Mike McKerns**). If you would like to share how you use ``pox`` in your work, please send an email (to **mmckerns at uqfoundation dot org**). Citation -------- If you use ``pox`` to do research that leads to publication, we ask that you acknowledge use of ``pox`` by citing the following in your publication:: M.M. McKerns, L. Strand, T. Sullivan, A. Fang, M.A.G. Aivazis, "Building a framework for predictive science", Proceedings of the 10th Python in Science Conference, 2011; http://arxiv.org/pdf/1202.1056 Michael McKerns and Michael Aivazis, "pathos: a framework for heterogeneous computing", 2010- ; https://uqfoundation.github.io/project/pathos Please see https://uqfoundation.github.io/project/pathos or http://arxiv.org/pdf/1202.1056 for further information. uqfoundation-pox-95e00e4/docs/000077500000000000000000000000001477757307100163415ustar00rootroot00000000000000uqfoundation-pox-95e00e4/docs/Makefile000066400000000000000000000012331477757307100200000ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = pox SOURCEDIR = source BUILDDIR = build # Internal variables ALLSPHINXOPTS = $(SPHINXOPTS) $(SOURCEDIR) # Put it first so that "make" without argument is like "make help". help: @echo "Please use \`make html' to generate standalone HTML files" .PHONY: help clean html Makefile clean: -rm -rf $(BUILDDIR) html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR) -rm -f $(BUILDDIR)/../../scripts/_*py -rm -f $(BUILDDIR)/../../scripts/_*pyc -rm -rf $(BUILDDIR)/../../scripts/__pycache__ uqfoundation-pox-95e00e4/docs/requirements.txt000066400000000000000000000022771477757307100216350ustar00rootroot00000000000000# Packages required to build docs # dependencies pinned as: # https://github.com/readthedocs/readthedocs.org/blob/543f389ddd184ff91dac6a7b808dd21697292fd5/requirements/docs.txt alabaster==1.0.0 anyio==4.8.0 babel==2.17.0 certifi==2025.1.31 charset-normalizer==3.4.1 click==8.1.8 colorama==0.4.6 docutils==0.21.2 exceptiongroup==1.2.2 h11==0.14.0 idna==3.10 imagesize==1.4.1 jinja2==3.1.5 markdown-it-py==3.0.0 markupsafe==3.0.2 mdit-py-plugins==0.4.2 mdurl==0.1.2 myst-parser==4.0.0 packaging==24.2 pygments==2.19.1 pyyaml==6.0.2 requests==2.32.3 six==1.17.0 sniffio==1.3.1 snowballstemmer==2.2.0 sphinx==8.1.3 sphinx-autobuild==2024.10.3 sphinx-copybutton==0.5.2 sphinx-design==0.6.1 sphinx-intl==2.3.1 sphinx-multiproject==1.0.0 sphinx-notfound-page==1.1.0 sphinx-prompt==1.9.0 sphinx-rtd-theme==3.0.2 sphinx-tabs==3.4.7 sphinxcontrib-applehelp==2.0.0 sphinxcontrib-devhelp==2.0.0 sphinxcontrib-htmlhelp==2.1.0 sphinxcontrib-httpdomain==1.8.1 sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 sphinxext-opengraph==0.9.1 starlette==0.45.3 tomli==2.2.1 typing-extensions==4.12.2 urllib3==2.3.0 uvicorn==0.34.0 watchfiles==1.0.4 websockets==14.2 uqfoundation-pox-95e00e4/docs/source/000077500000000000000000000000001477757307100176415ustar00rootroot00000000000000uqfoundation-pox-95e00e4/docs/source/_static/000077500000000000000000000000001477757307100212675ustar00rootroot00000000000000uqfoundation-pox-95e00e4/docs/source/_static/css/000077500000000000000000000000001477757307100220575ustar00rootroot00000000000000uqfoundation-pox-95e00e4/docs/source/_static/css/custom.css000066400000000000000000000001251477757307100241010ustar00rootroot00000000000000div.sphinxsidebar { height: 100%; /* 100vh */ overflow: auto; /* overflow-y */ } uqfoundation-pox-95e00e4/docs/source/conf.py000066400000000000000000000174131477757307100211460ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pox documentation build configuration file, created by # sphinx-quickstart on Tue Aug 8 06:50:58 2017. # # 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. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os from datetime import datetime import sys sys.path.insert(0, os.path.abspath('../..')) scripts = os.path.abspath('../../scripts') sys.path.insert(0, scripts) try: os.symlink(scripts+os.sep+'pox', scripts+os.sep+'_pox.py') except: pass # Import the project import pox # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.imgmath', 'sphinx.ext.ifconfig', 'sphinx.ext.napoleon'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'pox' year = datetime.now().year copyright = '%d, The Uncertainty Quantification Foundation' % year author = 'Mike McKerns' # extension config github_project_url = "https://github.com/uqfoundation/pox" autoclass_content = 'both' autodoc_default_options = { 'members': True, 'undoc-members': True, 'private-members': True, 'special-members': True, 'show-inheritance': True, 'imported-members': True, 'exclude-members': ( '__dict__,' '__slots__,' '__weakref__,' '__module__,' '_abc_impl,' '__init__,' '__annotations__,' '__dataclass_fields__,' ) } autodoc_typehints = 'description' autodoc_typehints_format = 'short' napoleon_include_private_with_doc = False napoleon_include_special_with_doc = True napoleon_use_ivar = True napoleon_use_param = True # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = pox.__version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # Configure how the modules, functions, etc names look add_module_names = False modindex_common_prefix = ['pox.'] # -- Options for HTML output ---------------------------------------------- # on_rtd is whether we are on readthedocs.io on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # if not on_rtd: html_theme = 'alabaster' #'bizstyle' html_css_files = ['css/custom.css',] #import sphinx_rtd_theme #html_theme = 'sphinx_rtd_theme' #html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] else: html_theme = 'sphinx_rtd_theme' # 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 = { 'github_user': 'uqfoundation', 'github_repo': 'pox', 'github_button': False, 'github_banner': True, 'travis_button': True, 'codecov_button': True, 'donate_url': 'http://uqfoundation.org/pages/donate.html', 'gratipay_user': False, # username 'extra_nav_links': {'Module Index': 'py-modindex.html'}, # 'show_related': True, # 'globaltoc_collapse': True, 'globaltoc_maxdepth': 4, 'show_powered_by': False } # 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'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars if on_rtd: toc_style = 'localtoc.html', # display the toctree else: toc_style = 'globaltoc.html', # collapse the toctree html_sidebars = { '**': [ 'about.html', 'donate.html', 'searchbox.html', # 'navigation.html', toc_style, # defined above 'relations.html', # needs 'show_related':True option to display ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = 'poxdoc' # Logo for sidebar html_logo = 'pathos.png' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'pox.tex', 'pox Documentation', 'Mike McKerns', 'manual'), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pox', 'pox Documentation', [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pox', 'pox Documentation', author, 'pox', 'Utilities for filesystem exploration and automated builds.', 'Miscellaneous'), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/', None), # 'mystic': ('https://mystic.readthedocs.io/en/latest/', None), # 'pathos': ('https://pathos.readthedocs.io/en/latest/', None), # 'klepto': ('https://klepto.readthedocs.io/en/latest/', None), # 'dill': ('https://dill.readthedocs.io/en/latest/', None), # 'multiprocess': ('https://multiprocess.readthedocs.io/en/latest/', None), # 'ppft': ('https://ppft.readthedocs.io/en/latest/', None), # 'pyina': ('https://pyina.readthedocs.io/en/latest/', None), } uqfoundation-pox-95e00e4/docs/source/index.rst000066400000000000000000000004511477757307100215020ustar00rootroot00000000000000.. pox documentation master file pox package documentation ========================= .. toctree:: :hidden: :maxdepth: 2 self pox scripts .. automodule:: pox .. :exclude-members: + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` uqfoundation-pox-95e00e4/docs/source/pathos.png000066400000000000000000002314661477757307100216610ustar00rootroot00000000000000PNG  IHDRI("WCiCCPICC ProfilexXy8?L-0kf0'EٗDEDDHRHJ$zs>9>ss&A&?J6#>eii KYy `Ԯ)79rLGm8B@2>T)KX@ ޻vq;ཀྵC= @~==~N#AY_B鲮Au\*i@6\PIK? 8h[+wo ys064#ۋPlilnoomooD|p`Ȏ, }(9Pa.x<I#5(gbflU 49-$N? /꠿ؖD$T lU%r# j3BU-SlzeƬ&*!+zkc6wlGԏtvjqreuSte=f>\2*Dm?cs:<8H,"6<"?xJ%6j3vgbXb |b|l%vq WSSCӊ{fgfd^IͿ~/>`K⍭t%enIޖzW\G+ԫT+=~$Y'X~_OX +_mz\ђ,ܵM][gcW nް/* S_x}P[?]V!gDnc}tcX8ç ']jui*o=ӄLwͳvS?b5,a+W=~ot6ކ]|zۨ .%u+Ef [+g .(_B`DDl+KKݓޒ5;"7hܪ눔zF 斖Abp0֨ɈOs. iK=+'kʱK6ٶ۷;:>irCm{,G/0Xo}PycB T܏=)R q7CR2(YbrJ &Jf*jG0,_G uNӗ52DM4YZXYsa3dhW`pM']]/<4q\lOcmA >̈́ēD-p!185;r!X-GHqSKXs8%B,]d)쩹tmgRϺHl=wxQ=;|2%h1cEimbΖ5{uO@)p P@@4 vLبxp3_X {&# (`a0_%MX3#l.gEVpFG# H7d@#JHJӁBT-jV6vN. 0}}# ;C5#=;#4aaJcZg1hcfbcgKbcf_ q|&s/e;Wo?!%+Xz!eL>'aVEDD$ J z"Y"U(}_YܢLģWED4Ǖ5:b3Gi]ж}H?!Dkbaeiheep gCK/uxr3[{ {{{7A'| ROF0GFv1vLy FRg`=[_TyQ}]ʂj:|Z&;5zg+^]v8)Wo3Ms= 2W|dMo5`H 0 eî`e8/{F ыF"Ld (M$M:* DKGkE{3ygMOLLre,,y{X D˱?PЙRKUirPcUК"Ȉ"h3!z: M8hQ#/Bo30 0j3VLL9,,ͬlمٟrrlqr@k;Gg:?Z=={L18jTSWP=!Ґvɖ})VQ U@_uAM@P#[sYO|i96ٌ`*{XS[WW/WaDcWfm_;^< ,{f[FF6u[B$3K2S}yz 'B<3@1 a <@ ^@ x &A^ ؑ#e&-)pb^Ȏ/t1]q#{GvJן֎G?&P뿙]K'$Ѡz{p"$~)ΜH $ 0Hn$?B* UZHMԧ3S;w,cM8@Έ/}عC P-VbB P 9qݶaJh:`]NBSx]ƊπΚegt ^qYV" YbVTTl%ϲb=k`T"AryDl]nkok}!۳wu (% ***K 4$QV (C`۫Z`ih.{姴T@am>(-I@dqqMdg vq>S' >OdZY~`($q>&! CC0R7g{0_UH)('XIt y2ǒ2a!z:CͿ"m=)~h3};پ;oևZV>we ZsE~uz`"t$!b~^NֽiΜeEjz/}ť$=JPQcI3-TR$voG>wqkgv)^_( Gee;lw(D,).ur@R#1dٮ],jN>$@OAb IG _:\F &UU%e**Wm+<ě/Rd*+,IpќܖΆvu. αɯK+k AḀd(T^ٮQ"6ƒIV|U,_b__oy%CF{ 1q:52 hGKq9@͒4`z D4t)fpaƢ@RؑOpjZ: IvX!vɔMmIx7iuNUpT"VB^<7_ \3V,[n]JTCl$btڥKPRf:|~Q嫡΋7֯V:U_gկ|}%$MHE}JWb;8dڶkChx_oUͷ>݊WcEN4V2 2qD Gji$UVԛzFTJmzAġH NDŽ >> ]s`Rzd$emf :{(I2psI2$y1Q` rʖ)? pH.&#s̱E>z(zT+V~O%S E`e?|p6O6GNWmחc=1.ʧڜO|.|^' zcs\@fZ+T<581*dtR+ m߿ȦmV2V_Ҫy^ "xo/y1S6.#.m}گvM.LQj@]u9L"Qo驆 nw-tlCHE$Nܸ$ĚHÔ"iLmIJM"IP(IN3{Qb`Wc@2_L'AG%q^7&MlR++eT0%wJ58؃<_ֳ]߂߯,)γɦ^0%tNJ$7~m{A~Eыkl7fVqjttϊ4z4@>w|M[F'O6JT:i9hJŠt^G|"EH@5;$P_ZÈ4t< qKf# !j@KFީ Jwj+/PЅ(@i-6ٗ8/+p `BF#*&Eb'FzuW5ՁQa\-69ȌjD'Q `coFT4u`]_R IE;/^>.<H%( .Ê6M6os !cFXAKXaG^p4(29I;YQS+) Ss3yR9.E*3]oV%t"Iz@4D sUo͐Yc7m0ɜBw on}uNO6 ?/)^ilû>-̈%WGN=(3n &Ou$J_hS D3}1ɈZZ 8*&rڋWXKt@ƚ*S$NkUk8.嗆Ȁ c!‛ M&*P=$Ϩ+YpIn6ǟ"4,Pɫ<%j:L!BeR "OD m;^ߡHHn #e؛V5kMĉUV\ч hFb%?)/B,dV91_SLl>?wBgX'eji&GZ-5RB Hbs[]a-?RVnθ㿬x^#\Ɍ{p N&4ΖVN1[6oE䃴 HrQ5ht4@ƝlxO#Euܩm~mnmH0.#F0Lt3NP$0)MX% Cπ(!T^; PX ED"}S*# [?)Z`]ulY6Iӈ*4P6ҝǩ5:d ]pHyBÁȥ/}L BaoF3^HA}3)-A/uc 5HCMx}Ix,[ a >D@c#_/~~/Yb.1;HA)\M\@})M'udFMq:8p#KMة%OE:p  )3nh`&pcbvi&I.-q&0{Tm$Jm3''~ ^`/6s DCep(:ꖁXfRB1(k>d[ui^$T8pT=R_$pLػ68ºZ"VsMmOOp'j a=kCmni/~nb5:\`dEQfai=nko]lvij>htHPc>Ƣ=z8|/?h?$ȉ.0ū(.r =P1GHq Z3ءLw6o\>mMTɀQw'=&re\3umů.5u+,Uϻ\ U$La:nL@B3>t5_<c?*^] c.66\omR͛N N ]H7JLAv/5z}NH7*̣U' pt<OBTT)D AŒ%I XPi>q)fӆy2(Y z{n6}L3Z` y{=dwmtT&;Vly_ف_Ob&$Ɗϲ߿]U hY$Kb|O:& mWT5"ĉ)UmcڧBCv}B'􇣩0Hnme >Z`2H;vtɱYFYHEJ{?L-sd_3f:-A+FYt ad;v*\Q0"p^Nf|ZZt!p +ܮҳ9#S`CEh4ŸLFdbO0n߱v#nTp!6|C|b8<>8*\zPyyK_tɀOH? )DtVy='CѮ={_Ϸ)ZXQ?dL %^2&$zIwuVau4 }Aݱm%iE2F},GvW%i)ZFHCsiF.1.զ.U=ˬEFwivlH6U2g2J16h|%d -vp>Z8qD]dwPS{:۬UڑzkHwڷfSxhi#ؼzwbTe+:3̓y$[W|N(S= s [?𯾔mz+ʨѠ|HL(4L%6Q#Rv!iq:tkvɀLp.éC1ugz$)d+>Ξ))1NVpDDh肃 9{h?kG*#}94XQ6^V&0>e;Amٶ/lP57)^i{1 @Nv%^J!`_^t׳~5FBc"Şc^gQ9U:z"*lN??Ts*OF 4V Oڭ73.ihPA6/sӸCRFM.߹c7BVhbGҌwGrM5"5h0f℉N6|@0`mYHj2Ϥ_{{%-CLg:?:GsCnde 8lߊ. U&"qAhPAl& W_}]~+/~ͫoUUX'j¢Av뉜d?,?eZ LTf{ֵXG| -d!mcT6i ]7p P F[/ Z?U' m*Ӂ/'lӚ_6 <){PpCX.S~:v3q%,_q@x%i[8UHx%ݯu}2 C{6PްYrZ KkI~'3@@-d@@З6bϓᛃ?4WʀVƪ"@ƘQP4lμPg0TXbPo[͋_gE53Nzڛ 緿[nz]@]JF) )^i0ajԊbrg8_DtZ!tk5O`X=JGHr0U6óe]F:,cqu$\RCe!J_@H,tR4ͧCeyfga{寴e6glIe6$}RBd-vD+R7lѣy`i,܈F"8*֦RR jR9Tv3W2u(V\3E:U IDATUg[Aʇ>C?`{ ]3- .q]\S~[EwEIa7}{GmiIc S2av?䙼s xmF~T;tw`$qLj5F&MҞUDՒAl 6hw̌ 88͒ټM@٬@iV5I._ KyYTOW*OBB@Igf R"4n:EϋSQ}ֹF]ЪԨC0Mm\%UR>,tWqCXU@IJ5H@(|;:ԚU*wBRg(՜"^S֎Eo>2D#?T2w:,SXm-kKi0wT:С^$\-G\-mݲ֬]#Fd%#A$˗ 5gvf.QGW|w^vh$y<`f7$zwHL?Pq}xsIzxp֊ʌ>gQVaL:4AӧLHg(Lˣ/;/)nL efΜiO;,ڏnV\iv\ʖTו~]z-=y-[nW>.̀ L|hyH2U bŻaZ[l+='^_ъo@sߏe?|MX3Tdwtxz,*V~VM"wXFNN.j[p܈@AFI-Pmxs; SIE WtC ̃Qˎ [%*Fʪg^"b2  F>XV#׬1#[+[mڄv^]mD9͆l;5N <nzUP3gzÖq|>_h۷mtlA<_ҶKlض6Jn@nmyc tK;$0T6ve3fҀLOA Lw$ā wv؟x/O7,ZϝVzd :wz-D<\iP%M3g؜stI/xr\r_AAU]8P`lcׯ~JmܲYwZ:zҮOtx-+|LV\iz9cs.,=-8(y#!}$I:9qC)N<+PH$ǀ $+8h t(Sa xyioeXrt?#5%z~c؆G=0TX< ௕Y 7f}pTbi($QNzB0z'IXcW<7o?GWj>c 20T?ye sE5$QTe$\,u2dUetlEqpަmz͛6B4:TLfZv̪8 UI^I~O~mh5Z ηB{R#1Κ!ZG h~#M68B4xWrG+x d -wiFKJ]!d-nͶ 8Efz:S*Zus܄qkR|SDsRW@J]8 Fޜm:2Ө8Uga~:M:y5ѥz/Zb(AjW@y-R^]43Kdy_O6G W>[ο?7Mcv+4NL4yLw< S9V/CWK !o :HJd5{=OY 8#Of'5_P[K{@9V^`_5xH6]>~% 5?[%Բ/!R/Vn|8z`!~ze4%hP*ɘ&t:Y̻SR MA Bm2AdHg9쎀'I.UkT%pf _Ial.f:0H< u ^J65* .yHa?nhAߏ>\=@ą4'~rްM Wfs~Zj@IZg[#,{T55N|Pٜ:udفYl\XbA]f0 턲Ń .]̻w#)17s0He:Fz|PoevQT4wѳNR8ɀM˚5@nj;l;6(}Z/K ֮@ iHObϻ;Y3%~ߜ 'YybFAB~m{H DKFq>@d$rP^D]N\D쎯 4+tgj.}*Cc"p #nЫ |t޻&ȡQ:a % Z<;HRk9\1a(F5Ja|83WEBozF!p8ԽEvHԝ֪'ߩILׁy ˮd4?*IYKZPWo6wc?`"c%>@xi:yqtMRձYPBߡEA6^^/⻑^x Ri7(O"(/ bk2mUk5I~D?Wy[luPwS%c:eKH ;}:_o@)*.% $Hg/ux4] 4 }c/SK+z ҬZ%9?‹l%R/g1S08EQkAM ='Fr_)!K] Pzpx–㠱f\"UO:S@1L= p LJ=t3{n|+; 8U^H瀩 (wsu `1QuEtv$oKh/{W ~N!E4}b>F%m}__W~(i,^Pғ$ P"K3}0qå%Mt礨<ô0> K})ħr`yI#ti)Heۚ]=B-4?_H6mJ5]d }VO]v|PjgYh?}=c 3u1l\M`]{g.ٯ3 F朘DGR(X4B?䤔oDo "2 Q!-v>gwka$EkN+G`P960R!{rIǥH"O9M($aŶN^쿹f3ʠ[nQ-WxI ޒX(OWnl\״t @/PbN鹰Z5g)N+_4NI>nk >w9J ,t3 zu=Ț/OhP84xs W%OQ(P#x}V VٔWmoz.8KB E0sMtF$(G_ͽ{Rh=ˤ.a+?˴~ꗾjwFZz2S=՛P8#(<@( L#=::kʨlThA vh]AA?9q:,Ɏ&;} ?+: OrI09|iCd)#>/{(ט5u@Η:6/hܨ/|?"]([!~@ 6hFz[y=9 ֦4y `-'%ghMeAdxA$o|0 a @'x/~/ !E<ՒfHz)) : ;8Yň"r¼0HH/ z4P @KV <. jL|B<F= I [&}l[|)\aiN?L;Yѩ{#6/mspL.v tU*S݋B44Dc]yB0YRxe^B[ϓΟk -+PQ`ZB@8\]&K4ol{ֶ`(B;qfv7k5*?6`RH1(k4%8ũȌ0R I E  @{I}6ﴋ_ y#PBS@Bq @H# " \ vO';]Njd< )KLf?ۖ8ퟛJTZn^ pOS>[(= 7P z5A/h tWpBW#Qx&q>7Q9Qhf IDATH'߉k s‡O$yA6sG{&>uˌnvmחnz[J CKTCHA})J+-[3t2L|!cDH ;m|ں7j3=NU۟dlt[ |`VbkW9[z[DW"PHw/}(BS(jOz[j4?a>;a @prQLHaB<;i!wOd1i鴸9HRbt! J;h P`k QԶb⅋. 鲑Cڍ=:wn?@ =4=T'NiFptKM~"o'bqw?C'7Wt1.+aჴ]  zӍ$~D*Ųv5?|jIv\S  9Vul/EOYuŶ~[w)|t*&m~-]UJEߕ8M׽޷רd{mN0sp!G00bFH9 ;  G&jEQFIWV17ѓS2TKgA-7{ 6M?k Z|23R AԦ8f~ܨO@ug!N5غ"A ]d29@JgYsϴ o՗^aN=%@D󯟷m\,ǽ{eU^Cs09 2v{Ga4HWK",<U_ER!ON]ZW[-WZW79\qm>`4U5e/@\=<~"M=g|AA#hK;dUژ>-?}wtp~hgVo`7,]ciSfLk F^7j" ,3nV9`g0gˉt:umD`LI'.}R ɪYz۶^{k^k_lk}s|~y6s#Vqj9QIxГ(0|GKm_;P01H[H, cBA B[C:lG$>몗JL0`l>J*-#1A[R,UE`Y)._}Ze@Au{Es3~NB>ɗ.ЋqeAZ" қ0u)%A6?H5k%Cӈ4^YKL? ۶Tugh#,zQjܖ঩o֫܃m?ܘ|QQ1kJW(Ufo@QX>f*ajt<huϻ^_3/uZ*6gnɒ$+00_]̙rF<—FyThDPt_| 6)^5@v'zGΟ1I rb)Oci$Y$AVpU Ѻz6:]Tpu W |wꪛQn ]W[[߰O\kE3%E,'tftkeF a `s}ӰaQ}r\>7hhWE'>ܥC:h' Jz&q|jV4GS 'ghua#%yL#qEW3ۦ^<mX–S?lt(fG{tس%0]lLp5 Ґ vTmMP? 'ilƎyqrvlÚ>9LN<#f.]ZuPja,~i@R\wOu 摤}((KZm[<(9:z}0!?t#a)$d[Nr^_zH h92$ݟG,㪘es²'%ӵT}nwtΑb>U ڠgq㕩2Q Ơ(O(eE_lP)ly0H΃4١CtqꅎДp: EE9X5ІY'„#x KH35~ FHlČ5;;](9BO;p;@zz.BmGx)h~_:gw~ͪ.^lc:wǰ8$p5G#T=b(Jl aD*/T<hA& MC"Q*Jg-n.ŨR]n DFZm   O&Qbl-;n-U_QuGc.vhG`EMQOH+i(N%ٜu'כ54ѬUB\DPH@UGU QAFf{6ɮS13(>x|O#5L$`tr$Wu&.5/+龧qB#J6q_+)42Q8X1؈*Zo J=O3Z:r DUfh)iPgժilK-Pry#(&!iJ7U8:ߥJ:,ؠnF:u#Р ^ vg%ׯ ..SGlǙH؍#Prہ_}OtDtJcI@2Q\׉FGaV3ÜcQ8ŵMiUyڊϓV0dIy,s%cOy94:]pƕ̺vnҡM#U8"ٌFJܧ[+ 6>oI#GuKgx}!tMN%m ,#}pXa{6jg{pw Bo/o@M}Pu<>||wp*㡞2jRqԵ r w!?ܝ<2r6HI&eܗ{鑺/](@*ő4GsyfAp*!C@ t~-6IK"} eұv)aηNi EJB8+O2ogs?x_f WC 3`%kG/A%hN'Ji"pFk: > zS8$vttt Bs@lko ҈VDT&q9zхݧ=֭ŋ<+pQ}P91byhu)&HsuWNh 1?Pg^IMm$ӎBR?NW'((e* V+-Om+f(fۿ}jʦ^vM|Cr$ l$@Ndc"IVGDiBlO~bOr3Sa[]颾W`A J{J7I 0Bdo Ns+u # Q)``ʹH'}[X5."VۨHы@*S=5P|^\y_PI+L5XOeuP;ѮRc#BNuXHf;_F]:.N5o{6$@9ĄA!8Hg@n߳z5Fs٢xKTVtWvϛ2SghJ 5Y up`ڹ|DpQVh4,TkrG$HlG "pn? $*<}aֵ+o/+O^=!)4ű>g!q𴷿*/̚W*!x$`8GyPM`[e='TWpKfR&U 1zL\ Qj2~GYyݺuc-Z?-.a:+Zҍ\gE5aJUX X4u7ýCrGꅏr-nݺU_DqݴqNqRuy ͛6Xwv&)VB:bz7ZvxM}ӹs45w5); cM/"GT`gID]Ds˱t831HkbLn^t|_ I;Ux/4Lzb}Ǭ[\q!ÑKJ@NĔ ׽u b"Os)^.$>4bL8o?V$.a 0\x2S'(#mOUH*rȿ*{ o^n ;l6!rBwk@]b2t֭[>2?cm9;HdB.^C zzen2֍֩S: 9\J#u: :w&nrаmrx945.S:я'OY3+eLuƨ}ȼSFk]>m1t9A2-vl2TT~ԥrV)hb6?{,kCΕ恽:q瀨Jyx4@PP-4ng$hAG$-D TH88.5}+4fPԁ&@Zʗ۰ ZʅHOwg*@TR.Hރ-P9ʼnc8WWFyF;̳2tXՄ hL#ƫOo}ᇭJȳacAhP[_o.ڼeҚ&a ճx=&C%0BO$h5$MpHzd@l8Td^tX-_RcFu- Ifp\N@Ј$rlyT4= QÄHEr' ^L@T=;:uk$Lޑ̡e~YN !,@ٙeھ)peT HسBc|0NUIoR͞6Ac9iLVgeMsTtZW:@:$ ͇ ԰Imf۫k)_x'wujPiبNUʵC<3ub1sM|sΣrfN /'I [N<[/8ᬐ"8Rꓱcn suh|"2X)@$BMUpDb6'bxh4b ?/!i#aF2Dq1Iʞ@{Ψf.n+g3BǦA텲7WIJ8wU6Pӧ8$;M|tP^:ذztY~ tMHPOu ;wxD )RdF`]Tn1઴cGF1cgn+eI/-ieVT*:G`UJ3Pfv X_B-Wڨ'jт>͚t?xUNk*_Ӂf; ՚ev:,ή=fq XEڸzu֑`ӡARDfi&8J[!iKІi q T]met3 F@(wf`@26&W?iWeYQ0ZURQ0"q! `)ЀKXGA6)(ay%6OSb66X] 82yRWݯ:Ou f34_Nh f͞e[l f؀G򜉟Uևͱ͵uvFaէE" |eu$?m~kTF*j[3Jm`bÀ&S5󣥐s882q1۞]ivEv^a/[z%$_zU5<w[a $ڱSftZ- Iy|Z1<Tݚg@: htvY>gC80 $#`KP e_!BOΠ{$I/ٳg۶m[>74nIa7mm.=x)F2 ۲~@W0ɱSv.Pi핉DHEoCӰ.sI$-2 JN ^C/]jSj4KJxāNB'ɾlϣ8+}q5=đ9l˼-7kwR]S*J.9'.h[ڣtQ~M5O*JsbJ9 %9JB9Ȧ#1 +ei'yz4O>%.VlR71˘a]ĩ"01=E{􎟈]R/b;Np$ӎP,g7 22#YNjRI1Qtr)P4+J:IDE3jD+LP|m$P~<}TGh Z (swN<$߃~)9M<%,K4۶yBo0/z .s_<콌tz^t+lC^=[rt`uA z8$I:r1to"|-ޫQ{79X"=>U4>m͜9N? :}c؉NXdG"ⵯ+W`I`ι{|j7v"8Ryt ^tI,=|}J%4~H.јGɑ 8L(!Ѓ{]%(N#YI0#AۮALYd5Ȅ QAp$|!9SU|gO…3%˲R?x<,@ z޼btd 'ǎ :}v}_Uox&:30<)*Ɏsz(tL ob|<&ynLIs4Xsye2N)7zy$z^}啲I۫W=Iqh$.eڬ ?*_.EA0%>vJ~X앥-!QԡDdezO4 8!ٚQ}aVMfȷ^J{w4g~Js@QΤ./%bЛ{߄ȟ{ӎ$zmuѱTiI+NO~N_y9{^ 珪^o*wv $ͦK t 2&n5$ RҎF)F]: CEGbZChH@! G #>Qp5dp÷o!@ BRNJ=2W\hjwJ} z<޵flɤjONpbVz\-C0!?Fx D_!M1DfDzIܳ~$qa5έ^00} #y8 qFs zZ!;ƀTJI8G^eݱYku_J8F)>;SX"ۢ킋."ڜ*w ѝ ^ʲCHfےel=vZOsc;[Ѡnzopw{$DҨ&iyğ`taLLFH x9DO40Z|VOI>h_öS9b; - '_dݰv4`~P< (=!q/8R1zt`q iIG\+҂V%I%m ?.a8FVˈ$Ej䨖3f،Y<8:h$A줺dزܫ,Z8vuv/N|SܔߍV)mEZUIH ͞s6Ht{Ϟ=uo̬YC=COڑ!)ܼ[NS^$+< Wᆰrْ~X'2RcKm3 "p!!^R0 gvl%8}nv&c0"V*m5Zb! Ip%ZfO`8eEu#4Abڧ,u}Mm>]Vsj3H \8'&~W,#1pCusYE]qU>hɒeޞ8DF,e1, śA+/~7Q+И iI2%H/\x$c.a5w /TKw <#9>32c<Gތ} D+ =)I#UVj Xb<#[q('ŭ,LnZm՜'$E ꄷݟZy4qDl?X:1+iJs|9/PIucz#xoU_q8V4giwW/]IӠCȕtl/ThrHx_š0YG+7 1`,7͟#*Mwܡ"wH)5ҏ̝aAT&fw_qrWkm@i AO^/2G[ÒŋEyc˘:#G|<E{ر 2k^r,B#oXt.@)Ճ",#GCUF*C׬|w]>iq6JR[g]?5C[V$07p]סhϹ Ww1OR'O>D9{#|"aw%.̟R0٧\3;^-l>8,T}R/xD 8DbS:ixxw,HZNT"3DF4Q(z5a݆ QG'@ e(=F&p'Z(2y/̉+Fqx&qJ"@ n79` $ ,$l!8eY30fLT7J)~1i:M/{iXk+堻kw\J")|4c%ŋ^~ы$΋GGFQW:-$J><IW:!);4y)$_Ño$G:tLRgq:)rwFC]s󭷪ԟ܋WYdbir.(lظ!Y&d|F^WEI3@8ÜbFg=[ڊ_7I88]'p)$HKW!ǜCp9"Q_*8`wG"6PS"0&7H1_f8Kׯ7y٢ B>+i94w]S]t?~cO9pEUk΃0 |bDa,ba%?%WRٵ[hP~q?99b\`)6fČ<5H@>ůz*+$/_R& ^.D腡;޲R&i d(X=pwzJ(@xG4GHwdqH5uFU<ҠJ`w0p zau Fd'Ц>,UNI~c$eRý/ G}PTǽ}/$pUCfUڸ-u8OhF:86ִǙfǙ=Q#X3#ynq,O03ɴX;gIT׼޶E z a(ÈEI21 _\ $< _7>D"wģw.?=˜y3 I͠%RJ(cEj1'`ɔfkZ,rJʌ {HNHԐ\uGd[a5ڻ+H̨@ RʙUK x{K3ewG+gol25-ښخ}q1B<=CocgjI~hY`y"R|ox3,m=?(ЇlA')"ӦM_rIغu-Qtcè0 1-DO\PKqޏ9a_,ч!+$過'.~%^wS孒02 H" ͜lL䉹 $,务0Y;Q_6.j1Tt_b<={ Bc>5ZܘM JT(DwX+L/Y9A~I:T&>=ۭ̔?0i0ed[=zy~g0ɕ-Hn+8D_իpktH(8HVsL8yKD0FT d ^E@(.w1,UQ XdIΙC^Lh.LK8ՠ,ϝLUH^SY̚YiP%'iS_(wIRL܎2K;,@Padzﰒ&z.w;80 Qas $ب]55>?\un7a)Vp iv0?c~G?UVlb)iAHq42h z$bCsCvlkȨ 1 Hdz֢yb7L,ڥB}4.uetɒiP9V2ԥ-v({w 'Lޏg?Aa!! 3I6')Fk1#RsoVgy0;wNx>zc^KDdf5 AZE>p8xW]m;eW^ sB!ͼOңET@dc3[?Q:#}oɅx`J aa>M!Tk#z὚ +6Fow(ke1IZ`l:?E@YmEru㩋./:mH$&M a06u%L: A.wp7ì8j쬗$x> ̓? C80 E.*@^/2U3LO,CY<đ't~H- oi)s8ḧP=]zbEʚ>$jC$PmGTq ~̵3LSYI_ʍ;shKS6nfhY4 )XzamwMGbVm5d&4\^ nQ*%r%PdB\Y3&pܩ6Śv00%uS]XVv:d㒜ܪߧt!ü' om+BR#xأiڤq &8ڈh,A2Ql HťY?{y=k h /o}փ\0^KXts<@Z?`,3Rc>WW j(NA?EV"cta+Ze2lnp|d*APj%2Arܩp25V#mLj$o%]ZX` 8pMacPDfsG|<"씽ϭ2 DuXᨛhCvNaLDYv?3&tq;]P-hlͱ yxmw$a+d: :c{-w 5 1!ha?!}2A [M}j7hD#Ed)RV!*,P&Xw7"xUù:x'i 3\ ,3 Z3fR){HpgUxsDRp)߆;,Zt؉(.%vK3FW@~y:#:G:q.hFC0x J2J „ IDAT )_;H{pȾ4 Cg,I:m~ FH6 u"}PپBIP2 D\e :zAl6!G I/k%ך<>Oh,K0s\FxPQj+" [ I9 ;;QZ(1*+4UEg٘o>rD {Y"q\H;vlq(W\9_Voא|vQ!Ez1 pJbyޱD.HGc@, L6ئ=.le}Πpm䨻ڛ}kgШ?? /Zb@gdH] ʫs%}@0Q$xGLF7 K wޡ3`<[ygoT@M@*I'$oPs`B1 s11_؎:_9+ lSf QRlj ‹ݙC_AX =5\0R9,+;{,DUDxN]щDpYirxC10-@Mu[Ct(FXa#zD9aQfzC6,_ՖOp4 gց^t=>iC\>y޶}%IND6uFde}K[KS`gqMϷpW_v I*x-ᜳSd&eƊ @2خ)O>Zq8e;z#|SLG+\Bxh(4G֣a1g(G >\ùcBem!+Ia? `XtYظy1#Ifƺc:0>o/аPW-X_QPaJ wz:\C8:MEBA^@"̵ШrHuvR]XO;E CevFd_y[tW;]vqQ Ef i,A"&q0Cj"!d3"C] 8Dۿ_͟tKpIE!fxcy-Ñ$Cvޘ ?4H} 4 Qf 9+[%bE"PnEg3:uJ&hGWt|eJA2Sj\&(gh~5 U  )%tFDlbb$56p9 Z:W[NjڟVZ4Wv&;$]M25\wn҂>`mK5mʜ^=/YDGη.%{9C!uڞsq2iOw4$2=p0)􅂸%Hi?duZKڳ'צw W:+z/?t[@ ^Qkd:0S7K{vхZ͎'v7d!jbqF@te 4HjD}1LhR%: KUJwjns _ Zh)rEa`ĴJ|,,`fxg InĶe%fi$4da&63Wt Maًꨛ1G{kނaCپ_9DM&JUz*L7NqaŊᥗ_n.m;YFrx̢, mhl-Z(}iޜwa)Rl@l&itjFׄ1C pSG>0Z1O EC[aTRck#t[A:$-F1'ifw$L` w R! Z/xSaᒄa)RfX O@=4M^$ߍ4S\pQ@:!FԿkяjgz`X׶ƊUJ7Վ B -AH>RaHWfRiȃ4jƎ*_ 9[[ojS7i!{QfckԹhWK$H?ǟ۪clG Of2)#YӯD|G@ a=#)yzEiGbP  Dz3uJpΧ9)y"YBO-˯Fb>E?t&s@te+Id$Β¿+ 3ݹח- / ~Z9pmeX.vY`t0tu|&-0/8 Heڬ=sXU_x`1@4tew&:n$_h  X!@D&/Z0k@G'P)޴ichyeυTE)0PEr< z8bE;$d~&W8rZB@jXzp$[yȋ R/ʾ*;tQ\K8F)U3 7 NQӕFӕILǪ7"~ 8jTFLPgwsf&1a~-?)|XVPfl SIa%,DV:'q'=U1=AJ+O޲$H_@rN V*$qYʁ4/wGNS)@kwpUfFy4hu*CXQnW`.Nut`y@-\tf_5d;dAy%pEq8  0Պ+kaogJ;XA^ /}07>dW24͌IZᥜI5#Xbĸ%2@ |$Cq<<.wHH82 Kp f ɥɡ!sz?8i If ٳU螫U]#A"]5Qvm۵X2dixvGU(ϕu:x$ﭖ*)ti+yo@“O=B҆ m掼}hϠeA2I\Y-bCؖ +R TH ﳥh~Ż ӧG&58H@|-NցDGԚ4F_MGsc:DoAfB] mZ-e?OӓO V(A2?)Рx+:]@ Lm21I\js׮YC4Cnj HojSEqsX>">.NxF]{CZo1,  A;ky8gi9 /o|tthI3@Hɗz޹ɽ#DSξ. Ks<0R+-V$R߮Ϧwyk"=}ŒY/V)s\.DXYN@#O]G)Dg!J1v?#2 /l jdv]rKD|>ܬFL-(w v*4[/epJ6R|{_6|oXwkT4[[lǟqXxqS~s78E=\}d0]e%I *FcoETP,5dAʾ'GI$'~Pι$,_FcL%q#~"cNTgb=GM7Nk&ሑ}Rb _TCPf( +K. ښ*OZdF$ӾSlۺ5lCGNPZ64eZ:qᐤ}с!Eת~lAGRm7^13TlI&}} dwJȴ$L5:3NG& GVGU\ )#p&/iwYduvn0Qk 1R8g*X^1V0+C$ ݣkb@F=a-b||cŒ I+@2GDhšW=zv( Qqea]'%Ow=RapB{嗭` H<ʶQh0s{@wݥLᩎ}ա]!:XJ9M'-!bzHH.-FVߪX{7D,fQ.Q FA~'s6:;~I50\x .4-P'DDL$u}8^DTGK<߲IL:@0=(*!/w>\ib,'I=`yi{bd$̕+%$F\!u=w2vCٖ WN~| J9wF9#*P/lg۷wOh\kjKGg7jY ܳυ.\2\ygw¢EΝ;ıw타2Z# 3tJiB)<g7]U;̔H[7kBsNdϜ93'+͟Qaޒ`*FfzL"\̓bB_y8&pz-H+tZD* LmrO(b˳@ +f ["*7P4: RtnOFq Y"_޵j~CI:m2zѠ-{$-(qaqO%vkzI;|Qv$O=~Zi׾W_O?C=TʞzNWD!pdahBIwu7"T{{qO7tI=QJpF780:RH~])?L4E9_X,̉*U I"/X+߾5!}mb K',$A8OR"l=*|O{ /_T28+,q$3L4BA3 IDATΧ05Zw/%\r<ڽWxiq/L7spט1 @9iqd2x0L56na]Ev(zR[ciau@m-_+s^jr0 (|w^Kr<:šaYº;&8L("F5|iH". -* z ɼd`T9S:7s(Mѕ>IB7L<8#!։{M7C]ѹ++!dHcQ'IJIiu 0Zt gfa0DšP!4sLMr4z3V;ח, eeKo~]u }P Kd]/6op;Z`>*˝ڍ*w=iJ$ςHG:uF's`??̞3'KN֠_edi,UV:8ZaB%LW )&@ٳgG "1cN!6./24ĿCĤ35i?n_&Gm<أW dݒ<%-WjDXS;Tdtx.\fV&H ^9FZ[&.e|Rnm@_4 Z^e 4d)@2s5Z62Q`E2&2*a ˡ1) IH,aA0r4*Α(cC8"g,H:?t5566GX:ê5jVs4䝗x0D'5۵}[x񇽢và]:{ƮDI uɓåx|^ (IHmܴJ۶P}Ns|?UMX)(( oS}Q͞5;\-\`boy)$a(+.]mF}kRdцf_oY%|}jқ Y:&$(_3PCC0}V9%vT%bc(qW2Ѐ/tK5 < MHpir =!Q2j eg̚5v jL5ŹڢasO?!ZyFtQ=%pLYqqiJ$ML/ۏn:eqv$[!٧Wtի%aN]QoH;ZG9WB=31E9ړpjd (_oT[*T}3TQ M}{eHyԀ.Z:,[\ <7IZH0(Ȝ$՟47 ;up2eT\ztߪuhԼpdP$yۦ>Y pM(RD =aJ$<0GmI3τ)OHzוW/z/yƏ+pxtl<en6`b WUwiD>UpY. ,R'h%Zj_ox^1F_4J>)wZ-ƀ=D]s] _B:_dE2=g/szǥ{=0veeϒn>~!Iq0!_l("9j8CHqp@úG?qlAVօ}4lܸ1;Be2z9[帞G9 ʲAWb1Ϧʾߒ~4g㏇ǟxBYZ>õ]tq]NaC$7mem:H7,Q(.J>v,zyR$sS' uuŅEV6^}G#Mh6n;Y'zU0JF5$$M(NJ#$/{t7V=i[tivcx\|.& @Dąމ"L}Eo nIqamڲ61NrND\=jH`jh;)e9sUaXfB]6nX6H`b-Q[#N`HxNhs{&&ؑ,^W\k葇Ȃw8u7hu+Ë jp@Ṗj:@q6m@qst-;O'iNEK.8ϩ k.?`ٲ2KwGm_R?gyVJ;LCظ₾+552\Y$ d"Zڢ|Dxi-},5)[6o <iOT> /{YG4K/3Lׯb=֓Hf4Iz nUFޣEweM QkȄ1b b  Yp`Q<[X ~~|d,Sg!Z?>򈆫˯2+Mk/2fxNNP~f4͔9ׇy01yJɽ;gpD׮Yl@7tz#H[:~ue Tyw Uñ4Ø_$zZ 0`8^t΍7,`8~!-\14RCbՆK٬=' b1QUG6z#WIig :e ]4$zM*:?px駬"sLiFeO=WPj=y`䙟=op$w]qU]Q}?ha]%ɒR[R5c:Jy:$O"%\>z.`k:H顃#5 RI[䢺c!7ChU X|[ ]#yavYʰDb'U2y<`(cȩ]sW5k7}塿~i8AY׉IL͒;4g6u=whg1,J/GE엢3ܓϞ5'lg^qpMp@a{;m A0M#&V2'o)jvtMd1KXnnG'?h9&=SX/vV,?XXlZd :0 8Q+u(]_C2U# ҜsZz%)$UzƀmnV͑"H[8q3ZN@utL],NO(!5P_Оsԣy+ <$zM˥tٲަ6C5ypPe5ք+VX<Y [(Cql.rvOֈQ*M勿꼦|A{#5=gHq(gi?k׵uǏl^5HvWZhPUKr-A1 =xD85 0}ƱXaq!T+O\C4|Ao;t|OUyTҕK.M%@obe ߾ǭ\"LXM\R(q j DxĎ7MH3d= ~Etq!>YtW,9t`K/jo.[󗳵1a:&Y:oqگ>sܰtabP/"4P-4+n l_u\(OE^NӋ3p.wE+!醾,zcht K55[\A8;ڭinIm:mjEm}3_)CKx?3RNɉ$M&Neg)i9gT$Iڐ[c=N~7£ X'X#rݸ.-7%^J]&xbBG.H&ProT.8YtWS-M\Whu\LOznfP>?=.s:dBD\&'@9LIv3*BM<(+䁸<:9OymqkL/ hֈҟ^!&FGʗ;{$$)n2JIr Y- c1g=RRcmMmX/{,s0=G e@E؇=s_8KCNMzn(Wtﴅ#EMzڳ6!~- :uy<-A Y[niOWL Fc^R >T‰4}Jr99Ο#d1cADT@04=@ٻ=䄾=F1 ^Eq;Ja}ɂ!q)m&N HO5E3I_2;"GV;$גV8:6F1癛R0yR2r=yC9hd,rUٙV9`(յu``7OMt#5b}jcRajw5*䃽k7nܡeuXh]Xbu^\1YSSI $Ie[1 ? != $(\E003Cvu# Vj:BqߨDYL;! ڸ?\sML)=kV@<$Hy%Yq'P2YAJ?/w]2'-[>EW6ԊcU0"A8].e@g>SP\wW^~D$#j(+59Cmfc@wNT9,&YL.a`! URy=9 9LCpˀ;^-t\It:R"L A>4ksfhQFSِ3Scprl٥HG w5@ޗFt"?3ԁe>HS6*#HIuɧ nt8ׅlkBՔZFz7cWN=q]tt|$sS8tޟl*>y5-,>o(@flrTFs_y"&gk5+ݨ{KRQIX0OwĴ(OoV#0 F$0kJdHKY#d@ Oq9ѿY<5prZ#4ۢ#Ie4p59UUhjoڼɋ*G|5腺9"euo3LW#twvA6A/4cmB !IWh 3ca$M3nI O~,? c/ oy:N)!w7KUszߩv F2 9lo #d[&?a')˳MA׷Pz QTk1*6%1Y&/Gy6kd,{e-f ĝj$ 49:fä6M[:ٶK囬-EqʮCP82y*ȩ#qG, 'GY+ Lg$,<ۗ<ӷ])cpnU(8g?^|9LewG󵿔zH>`sHhpwLѭnŨLEɡٷgD7 $0d4Y+ @f]+0Ž@ Q!9Ew66{ʾ=[%Zi_zvMטaOudIiUĽFIi3[+BW*,?3̈́E#trkp/C&2\ddksKYasݪ(= 屘"i">{dG(e&DwEH84}Heu%!7{,Y~/f86tM):T0VYt$4pp: Aa[7rZN) Fǥ+eY_y=>oٺ%zk }*]Jc=S WYBf2T#H(\E@C䵸8?՞-|٩{}ѡ42FpJ_ 3ĞɹS.fu`X["iRaMƳbt));7 imO`ULKb?6m^{%J|@_j[c֋dh=[p$}Ko( %*YfޒH[FwIP)HxDtƙL sՇ/7D; LLDBbO\|Pb%ag a $r \i 3M1O%p5c]<BUq-ݺ^H!Käb[H'189XC3F@S'HqN76rleY;sUz fJ#vBTy)Noph"$0+<'tFP/24Ć I!ͮ)nݺ%({'$GJ5q6!1jEr^%A7=NEAA 0\rIgU~0dA#)\aH~m v#x`X0GF P׈‹nj Z^f~2J̕G cb.hrTD5hN?utH@o57 A2F"VFjL?q)8G D`9^ E?|{:kr0qw&*̓e2td@GK άW1Xf%3T;6BU픨2( 3Oi;"]1ojfN S^”TYita?%R88 D+_7ӆ+$yW`h=/y"^0%JUʣ@wH52Q`Ii(Zp8"gΧ1=b;FvLĘh(`K=͇kK8n iHgZcgܸah_CwV_.a,H&4#؜/1Z"izLKz Hz&jjB₝:e{N_O?Lhޡ sELL-D nfLB۶mMXJZs\VH LJa:sRCeg9N›gHeXG4Dщj e@*٦XآX}ۮCS?c3C1Ydq;t|Ji=42hźBGll Z勒#a(aѡSQ^_[E$D vhsφIJ'),OW.ZD(M[66֠1QZ(c>]^LAݤM~tfÄ lC =Ӹ0ac" DxDs xp&Z1+5u5-lT~2zb2Yz1\:y _1s~3-Ny? /!*ۍP%j*RzSP4 W,iy#T;a>Vg#Oj%LYtuT_5\TR9|%dYy%pɑ=o9=RW: yϟ=Γ^6>`-ˡ1AYyAx3ǮWG}>,<4L!8hcaT)CEd.JSNbC](3u4FGr`[QG1Rpp 0;/=tHa R! ZI2 !&&Šgׁ/]Fljz> Q**x[EMdP=mVФ:ʖfӡ* aZN<^5/^o*8ҹIGA0\amr&'HH% ,5m4A%Wi9['_ghT&9cƏ Bމ @Va(,9a)x8a嚵>)ouC's&N ~V_@ dc 0Jfs5^5 S K3iZArL]E][Ш>7*l΃š埇;x]+[1EzSr])B]+p?xĠM%p& *jåNSdž*阶>Ε-cG\,FT,S~Da޵HvԓO#bĂqɒfئm(GōlK'Dc2y+9N6Hn#Co4?9MЋE0*V PXaRfŽ静Rb߮c\G[orr`p0$İ~ʲ-0 bo{C%-IGuˌH}uEEuxY+;eZIbsTջCua!cCs8$DD;>+# JN'#-aVjjES0T#S ZsI߲#u*v+6vK8T'TBd+P#OClSS2Qe$skEEOJ[emFTh ,)V3݀I*h3kCC}ysEur3kBq">ޘ  ,aH -F{:HI~CMl'qHgΚ-%a5sb~#(B)_g1 aFi~TIۜ ;ZJ6 HwRVR8'9֊jvҪC>ŲSTa/P*YY-̳M|L*Q5])nS=Tf< 5RTGY)wjXʢҝ {4w2\-xˬ;V\tmH{:-=7e: j ?J؊ܭCS_0axԷT&?Hؔ6e"W!I״Sq{Z}yORFjQm3诃B2"}Q|A&,WKce~vl ?TRKJ\s I$'Q.R;':VkRbJxIsg -MtEax.na<^ k`?/jtCן+RXao:J+&p .Fies|P!;>ܲYCSaJҡ.OX)Ʃ uhZa%G@Zx>XMx/;ΑzHPa_Hè`vNܧ͇ $G)u#YgϚ.)wb&Թ wHfV9⽏kw(l%ϤqꓺiohY^&Xg+IaU=UQ? l!hһɊeP#93' i&zMTo[-`Iu#.{s? $"h!-JTqF܋㝿q$5>,]$lBL&%@ oQ=gvE*0_X?y?UƩ{:?kppjѳO?c7nA@A1Sap06g3RYA6WkE%49E2܈܏ngoeqћXF. z pOțϳȬY$R>'KhMpZ! NbQRz]- 3/X> K7dj%N9n!`_z2 8qHݻ8 gC,sB>q];ث)I:!jMOҒȿe\Ϊ0T҉8Ey!8:(1cyQ;޼qX,_E~g &M߻g ul8⠈GB|-)uW/R[U❋وյh_=qssU. zXحtX HOc1bv2>Gv sfٯ.鳀7DWUՍ^۝~QGJB&?y|jM79SO>맫Pm5-vt$ks4[|vxeE Lq8`< C$h@U1R/[߶{G9 xVWORi+/}Hn9gGI(,Y У ]lj= LZYxB);@z*oJGRF=u촽*wMgS57ySva;a\ ױlWx}*S-Dj qȮ'-/ʅS^۸h#eY8>m<ӾUku''r_x@7Jsݓ'OqNMsQ\ IM -B|┯{Ǚ/bY^ Cy /bm$}U;^h34m șSbތ$Hll PgNuTXk/}w~?7L3A^:XzpUy$H.]fjuPxG9cA]@# Lj~k[LI?R82N(GE+Z!MsWa?0/ck9L 4<ɓ~FUvy>̈*lӁkik  IDAT'sIY&I ޫX]Cp%'dYaHN:0ze8ISm]uT8 @@7bATH1C.wR-'ϙ(F0b뇿7etTa2yWk]x{$Mm"Xz:K*-zighmvcS~fG!@= ;e$f8io:1Avd;DuP PFh P8~L?ϯz-[A?ɪ?/m;MsH3N/|@UXPB<NvǬQ!]N?"\zEq$伬Psql l@OCGw-VV0o F& DYke0UZ`L76Aʚߏ\I$H1b3F=ٷ|Xϋ? S{O& LeǂO?~wo7mQlu=]t 7jNitM{ aӛ < =~@| 3Njx īޏBi[P䈋̗1ZFY}6,.KM顇ķb~XO/":zaBiC٘!<?/mb2-1>03T.] F7-%\ ڤiv ,eOjpqS=ʆ'}߲7I|Ÿa06Wc*+W^LdygYqL7,Hz!Dt k?kɢ&}Ӫ2B*CM!=< Ã|_er8y*6GЌ7m˖/P2X:8R`Vhil}z܆dY߯9y왧 NCɬzwOݲ?.m_=$ɟkkYsE֫ P,\ߪK:^ Qm(_ s/  cQ,<1]<2|( ̏d_ʗmݺuVYk105M#@n銕n>*-oT%M 41Oܭ<]e7 =27tK ;:+k_ъ/9PTCE/_ָڵ6oH@xf,Wشi3 C驳gl"WFIAU2 6xB0 s (q([*pRJld2u˫ A@gAs~ vyI$OG(a+;Fz?G]fE"Nb1r&L)A^%!#>2~//P0Wҏ,)\픒fƍc`!ߐxZp4"ď@@7rHz @ kC&x9z k뮹td@9 ѭt#6{172Ԉc\vKnfa;h:Ycoy->n*6C$bntK." ٲr-p]6l+r"j7Ξ5 }w6>2H1@c Îvb*/DQ~y"sdRbF c!^crDc|%[v6hs6\FqxBO`"G6p|taHAF`DX>m}N@FwYjEFnj3̰Cxy^3*9u.\z  Հ;'=#@\@A<'|3Uڎ׬UcI%GE.k%vѠ\slaQLR8sy쑼~2/Ǔ `2{ǒƕSr;@D=xxr&l ܫ鉃׮7|kl㵲O{|c~]kɼ\oC| ['208RzG?@_#q6Luƀ0٬:UpU?dҖmzS)?P@ҡH:mߦA+!L|o6vlcľbMZق-g.AU$cF)?(h~٬FwD{%>޿@-MM[d-[ܗҿRH@^wfuÇiۮλ[N, vʎ&#I+r%i3[TlV/D'/+,(>[T3)ex9C1uD8XLru@Rm~Kd~WhG |o֞NVx7D8r:if[qs*۟2u-]Ė)#H]{,Obh+#4FɦlvSQo{qn{?`/dQ~.d=ϣ6p ׸Fŧ?){饗m:,p13wͣSqsf ^@k;95 Qp08t;B 6*|Gd;a @gπe쥋8Ԇdx'*g>V,9DI H'&QVGFp8ʹsOdxosp[! ÿ~ 9H8cE<0Uy6CT/qNrVg8_@7OOK伻=G@($0g$%nr+|b܌~;m8{ӱc'I7 xM?aoɓTC&v7Y]\9Sc<^Ư_RŖB ħpaH Ynqt`L0р-J8a#%~ϝ;ߞ{} #{at0 r6|9NnHo( S5jLJf[&'w2?y7’Rba:5kHIC<dScڵ ek[$x) i L?Q_| '>DA)$#xA9{ɯ V@aFe^~-{R|VFefh(.1xY}1:[@W>Eϱ<9᥽3jH4"Fklj۶3ND|x̵ڻמE[=؏9ptՑs#NﴱEK6Yy,HgJJ9 !jywnjeY?Ok&eGeЀbHnw1ARi5Am!W_Y>ao|ѯj [lZlh veYsP <{md.2e^̨uL Qq(ihrE^qx@֕V@PˠRFyxD 4uVZP!s1pnf˃T c[O}Ȝ&,Ss@%.MK#qSy1ʅu$1_Gz۹iy6ۺu(O=nW_xZIѾ!%3F[y|J4C{<S.stEѻFc|Y5@2dr` rZ@1 .!ef4p npm+rY j0 *ďxfKt圗PEf7J^_q .j7ߨ#%0%< 3}5*vQ1yr Sgp9--$vkC[{Hq1)P9k?z(?+Ivp(Ja̓bl0B<$/Nה O2E`U*/Nw8ب:cBc|6~4R&-efcݘ%K_llOo{*ҡ22 9A /j-/ăO؋gb&hW\N*.\ ܵ&b{B/8R\o$bw1A|ʮw+Ta%&MJs簫ltnpz'rpjqhwu[Ul۶ժV{peevUL73+) mG{#ƍ(:!Gډǝ|L{Μ:m{d3列{3@/@S^ =JisJԮo#5 P PQ$AǺ_-]۟J|[`8!'! "1}uu 4Nvrp^A>p28|bgcw7mf;"U?-`+c Щ[(v]m=bFFˬvE {̿lH@ʶ4omP7w,ɒ. $6b2h*MT+D;?_րSHx:v.yH)M޲J6@b3 y 7j&۩"M_4j<{!WgoUhh̝cӴf-NS2:%l]=}ݧDr|l*>+ԻpB"klmq9. @8LXG=Qua?(q{oتկ"(Ba,rtGWu<+g x,GHcB`Dq"DW|re:$ mAR,k@+Qk=D^iJ%2%\s̮={T*ZGα&Mt8Y}qMFm[;V.=^A, qs.|;"zeqxh٠ /f7(_"\x-irN)NIU"cc X2'A3͛6i/]ʏN\\i0@α .q~ljҪߩw =d Jr]G1q멪/jZ'CHh/i#*sL 9tFaÈ|QF%o8OHb>tp5!]`U'i)漢~mngnjr.=/_ܫ}9ɾRˁDAW&iOr6 z\ Gx-7w5X|qk"@@C4~=CyH0 / b _aMuv.DQFh 6gZ(G5Gɦb{cqgsƞ={n}Uj1skayh3M-\폢.28fƕZs 4jy7?KjI}v7 +mn'e:AKfG?{INТ> cޏL pq)8ɋK $eF96I_wP IDATh^ŁSX^%d;g5o1c2E~VhzEߙo}bE >HJc{jK_bCJ?d_OWq32i欰7_QjS1+pjUti͛Ƙ467ʼP!q $,3ќV'qx䨑r:^9{{F[3)RЌ\dƴ:XF@}$bs;JL@4Wƞׯ}RH˧RNbYK$be^06 o93=1b1, 4$Wᙩ(#4-d>_:tH yh_A1fq9ZhU{ߐ΂LySylN8~l.ʞ{Vyf)$[L$j"qҡ1{ő3grweyQ+|>O9J R Nkp<_gNuM}蒥`6ǎ\),qsk?>(S<Ìb9O҉c+ 1rJ 5MBYAjW27# (33%Rc>4M8wJƜs u<&| $/wQ%Cqgi3Y>wsmXbB~0$wkn& Yq:GqgexҞ<06xܹoR&EMbQbz}*iN5O_R#՞@<5sր3* 3CؒQa?:]XO},1&&׾7yH.8:JcO I"9Mmix|kg] p3x o(_;曛K)۲ȉCD<".ƵMFvm۱~;S2"`mN wmUʡZ*y\.xɋ^~zGYacb񢅋˛f} +ɼ0R>I8ה4OW?Q#KM݉SxsP K9 ZO'O#ؤnLx~G)v-{n8hFUy͘Q/p!DpIz zg>cq _ 1纃y1M?g짩Qhq)ªl7TϓqӦMu|r'k~xP>?{T%'TH2*0Q jgX{|-7< uܧ}b0KJ&;jǽqҀeqP`hsmIy2 v*ppR6sg*TW(@rX~F[|P'rR9=i'=qrQ;믻^{ߵm4jM&Drb+Yg&G3gEt@E i۴F~nK+3|GtS}@S 6ͷ|@=uJysLkoymٶELsypq,G.(N"AF|8a/m4,=w{c\z(@2{!nNLmoys<*eX}@"wP0u0#P14*H6$Dl*khmop9"&0X:I3HJ\fgDQ~ǻd7<&,1Wlds}]yț%K]Ͻ}|dԤɄ2 E@R ףF2[([gVFk9ub"H7oC-1}C O-)k͛J)@"C:IU HGfi|.]"mp8̚8 ұEp "VԴ[GaSiH5";p 5pi-(B)HR$]A5dpS#iȹB Q sB38G\ ۵6;sCfy)S\b:GdyC SlRu:& @G"A\9SbLWaUAt\y5 nDU/MӠ{/X93Cc[z}Cr1NJ&BsS ĥ(@2^) %|w)Wcy>y8d?J @v-v뭷~oo+PN$0(PLN!Q`@@L\b5k۶v:mk= ގ*}i Ѱa@[n@7ǧD(@'ʤQ[{][jmڻ{0 q0A&IAα׬;~oށ?KҔ8@.=N:!!mF@vΏ?a^hň#Y]!ͺhgæFq")ۈR92_ $/Rnxν5U/h[lv#GQqΩSH-eoZ4:T'[a1Fx X?Z̆{׻YS]9bb$u:&  $K,"0ƛ\Qcy֭[lͶl>~EquIKӕ= ]w<r̘6AwTm+% K}tL/HR)y(" a) pNNƖn#2*IuuP.(]r}KTJw(Vtޒ j:FA>,;hZMVw*(Q`(~f3O@ pѪ;9 =j8QH:rVfm:Vfm[[}nQRIY3&y.9rY@%(&g*ATG7 N3g9NMcdZ#G߰V(Y;m RY;SH$HVک.@z#vwV@7;#CǣOS 䠓8UPJ n8-w1tErH޺W|;5/Y(Pi $4S}Rz /FT ̤"AM?A~ݤ5s/D R d +gd7]#Fs&6(:e_|ҹKW,_ϐ~*@ rrlQ%j/êXo-@ҥ ơ "tr vbPN@)r۩b4DH我Njk뜜.2׸b8ew@ 5Z}ⰵUGB@sJ$EHʏ?hE bpKlmTCW̫dCԖW \% Rsx+ospl5B;h7@5l$ssG}\e\-vTWuL-IvCkīXrd"ǁNs}O'q07$fm؁{EmDNm@p^S=yI(6.)N>N~F/ {{:GAe3j#N?C $̫؃~O\C% wV XJMwPZ۞3^+ip@P$]r'r+i{L$o'"1N'">p~7Pnf@7[}4+Vى/ yLpZA|pl}=! ]I"8;K]-}nRiˎ>|+>h1y+b9mD"Hi2 tb._;a]h(.QDG\ Z;loRHEΑsDit5@T-vTJPZ8a>1Z~A3tvA(2VܬR3>ݿvJ54$H@Ii )yͪUK}xFgm8-wS\\&!Wܱ^#lPzu1)=M3NPs88EŋdҽW"2Ɋ}7VX]5k6Vr"mrGٞ~َ>ݹiq5|T@P rm/8HՊy6Cѷ׸t%l >v8(tss3G_f .:fXaI$kշda>79pRvWP:]~{vٱd@xԍ >hu#^WH 9@OJ][;' .cN$3܎.:lCzDd h)S7pf;-GPPu/jT*p]n`OpS+eUcFٰsk?q:myy3Fk liI N#GCU=[g[Oɹ#ZN G# t:xW ÜgZ[s\/SNEsY:.\2~kV5Q7Tť0(CӣFjk:%"ژYˎm?:'8TMt,9^~`HHVuUpG6sæ_wa%+dW~FaMR6DuzhY$SumMlYʹF8RFhSHS qyjsj](:WPh۽3Ose KsKIG^8?6sq<*Z6JvxyTSc(>\\Z݃%P63h#W\琧#=RH(Ce2ԣgv85U_\NdS t6=o2+1GLqyqkwO٩WwɦTVB A@[*̅t,cոZ{CM`nE6zIePRkó()@2R"QyI:8,>k?!-IDAT%?έ?+y `S:N H(ImĜi6&.s]Mf?5V%PO8gGݰ:ϝ֣Z9J5WиvU6zNQ7K ąbPn;𜒸io 9 $# 08U9>pEN Dn2Jɝ-^$9|BlJ\T]^G^geУ ʶ,>˧j"p^u~ ioX gRJ3ysO!(DR leõu8A9ȁ^2ܖ$N&,VV8RH]Y4:uzb"<ˁG`!:k}G~^/7Y-6YȖN0=|!=zw $Ai3Bk G͖vO-J)N0E+[2XD.H 2p|W28N[,Q# RuV&U p9W8&E^O!Q,!H P! dӋiIA^'En1J"@q\|x98%̹Ev:'$B@@Ht,P0 J^Ep8lc7`>l X@ ١yL4.blLxzJ/[r9B"?!\0B@@z@8V@@@z@^n% $ $ $L} Q Q Q eIENDB`uqfoundation-pox-95e00e4/docs/source/pox.rst000066400000000000000000000005521477757307100212030ustar00rootroot00000000000000pox module documentation ======================== shutils module -------------- .. automodule:: pox.shutils :exclude-members: +getSHELL, getHOME, getROOT, getUSER, getSEP, stripDups, Popen utils module ------------ .. automodule:: pox.utils :exclude-members: +makefilter, getVars, replaceText, getLines, makeTarget, parseTarget, prunelist, prunedict uqfoundation-pox-95e00e4/docs/source/scripts.rst000066400000000000000000000002021477757307100220540ustar00rootroot00000000000000pox scripts documentation ========================= pox script ------------------- .. automodule:: _pox .. :exclude-members: + uqfoundation-pox-95e00e4/pox/000077500000000000000000000000001477757307100162175ustar00rootroot00000000000000uqfoundation-pox-95e00e4/pox/__init__.py000066400000000000000000000031371477757307100203340ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # author, version, license, and long description try: # the package is installed from .__info__ import __version__, __author__, __doc__, __license__ except: # pragma: no cover import os import sys parent = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) sys.path.append(parent) # get distribution meta info from version import (__version__, __author__, get_license_text, get_readme_as_rst) __license__ = get_license_text(os.path.join(parent, 'LICENSE')) __license__ = "\n%s" % __license__ __doc__ = get_readme_as_rst(os.path.join(parent, 'README.md')) del os, sys, parent, get_license_text, get_readme_as_rst from .shutils import shelltype, homedir, rootdir, username, sep, \ minpath, env, whereis, which, find, walk, where, \ mkdir, rmtree, shellsub from .utils import pattern, expandvars, getvars, convert, replace, select, \ findpackage, wait_for, disk_used, remote, which_python, \ parse_remote, kbytes, selectdict, index_slice, index_join def license(): """print the license""" print(__license__) return def citation(): """print the citation""" print(__doc__[-491:-118]) return # end of file uqfoundation-pox-95e00e4/pox/__main__.py000066400000000000000000000032111477757307100203060ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE """ run any of the pox commands from the command shell prompt Notes: - To get help, type ``$ pox`` at a shell terminal prompt. - For a list of available functions, type ``$ pox "help('pox')"``. - Incorrect function invocation will print the function's documentation. Examples:: $ pox "which('python')" /usr/bin/python """ from pox import * from inspect import isfunction def help(function=None): #XXX: better would be to provide a list of available commands if function == 'pox': print('Available functions:') print([key for (key,val) in globals().items() if isfunction(val) and not key.startswith('_')]) return try: function = eval(function) if isfunction(function): print(function.__doc__) return except: pass print("Please provide a 'pox' command enclosed in quotes.\n") print("For example:") print(" $ pox \"which('python')\"") print("") help('pox') return if __name__=='__main__': import sys try: func = sys.argv[1] except: func = None if func: try: exec('print(%s)' % func) except: print("Error: incorrect syntax '%s'\n" % func) exec('print(%s.__doc__)' % func.split('(')[0]) else: help() # End of file uqfoundation-pox-95e00e4/pox/_disk.py000066400000000000000000000102561477757307100176660ustar00rootroot00000000000000#!/usr/bin/env python # # Authors: Gael Varoquaux # Lars Buitinck # Copyright (c) 2010 Gael Varoquaux # License: BSD Style, 3 clauses. # Forked by: Mike McKerns (December 2013) # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2013-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE """ Disk management utilities. """ import os import shutil import sys import time def disk_used(path): """get the disk usage for the given directory Args: path (str): path string. Returns: int corresponding to disk usage in blocks. """ size = 0 for file in os.listdir(path) + ['.']: stat = os.stat(os.path.join(path, file)) if hasattr(stat, 'st_blocks'): size += stat.st_blocks * 512 else: # on some platform st_blocks is not available (e.g., Windows) # approximate by rounding to next multiple of 512 size += (stat.st_size // 512 + 1) * 512 # We need to convert to int to avoid having longs on some systems (we # don't want longs to avoid problems we SQLite) return int(size / 1024.) def kbytes(text): """convert memory text to the corresponding value in kilobytes Args: text (str): string corresponding to an abbreviation of size. Returns: int representation of text. Examples: >>> kbytes(\'10K\') 10 >>> >>> kbytes(\'10G\') 10485760 """ kilo = 1024 units = dict(K=1, M=kilo, G=kilo ** 2) try: size = int(units[text[-1]] * float(text[:-1])) except (KeyError, ValueError): raise ValueError( "Invalid literal for size: '%s' should be " "a string like '10G', '500M', '50K'" % text ) return size # if a rmtree operation fails, wait for this much time (in secs), # then retry once. if it still fails, raise the exception RM_SUBDIRS_RETRY_TIME = 0.1 def rmtree(path, self=True, ignore_errors=False, onerror=None): """remove directories in the given path Args: path (str): path string of root of directories to delete. self (bool, default=True): if False, delete subdirectories, not path. ignore_errors (bool, default=False): if True, silently ignore errors. onerror (function, default=None): custom error handler. Returns: None Notes: If self=False, the directory indicated by path is left in place, and its subdirectories are erased. If self=True, path is also removed. If ignore_errors=True, errors are ignored. Otherwise, onerror is called to handle the error with arguments ``(func, path, exc_info)``, where *func* is ``os.listdir``, ``os.remove``, or ``os.rmdir``; *path* is the argument to the function that caused it to fail; and *exc_info* is a tuple returned by ``sys.exc_info()``. If ignore_errors=False and onerror=None, an exception is raised. """ names = [] try: names = os.listdir(path) except os.error: if onerror is not None: onerror(os.listdir, path, sys.exc_info()) elif ignore_errors: return else: raise if self: names = [''] for name in names: fullname = os.path.join(path, name) if os.path.isdir(fullname): if onerror is not None: shutil.rmtree(fullname, ignore_errors, onerror) else: # allow the rmtree to fail once, wait and re-try. # if the error is raised again, fail err_count = 0 while True: try: shutil.rmtree(fullname, ignore_errors, None) break except os.error: if err_count > 0: raise err_count += 1 time.sleep(RM_SUBDIRS_RETRY_TIME) # EOF uqfoundation-pox-95e00e4/pox/shutils.py000066400000000000000000000431311477757307100202660ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # # adapted from Mike McKerns' gsl.infect.shutils """ shell utilities for user environment and filesystem exploration """ import os import sys from subprocess import Popen, PIPE, STDOUT popen4 = {'shell':True, 'stdin':PIPE, 'stdout':PIPE, 'stderr':STDOUT, \ 'close_fds':True} from ._disk import rmtree MODE = eval('0o775') def shelltype(): '''get the name (e.g. ``bash``) of the current command shell Args: None Returns: string name of the shell, or None if name can not be determined. ''' shell = env('SHELL',all=False) or env('SESSIONNAME',all=False) if shell in ('Console',): shell = 'cmd' # or not? return os.path.basename(shell) if shell else None def homedir(): '''get the full path of the user\'s home directory Args: None Returns: string path of the directory, or None if home can not be determined. ''' try: homedir = env('USERPROFILE',all=False) or os.path.expandvars('$HOME') if '$' in homedir: raise ValueError return homedir except: homedir = None directory = os.curdir while not homedir: homedir = where(username(),os.path.abspath(directory)) directory = os.path.join(os.pardir,directory) return homedir def rootdir(): '''get the path corresponding to the root of the current drive Args: None Returns: string path of the directory. ''' return os.path.splitdrive(os.getcwd())[0]+os.sep def username(): '''get the login name of the current user Args: None Returns: string name of the user. ''' try: return os.getlogin() except: uname = os.path.expandvars('$USER') if '$' in uname: uname = env('USERNAME', all=False) return uname def sep(type=''): '''get the separator string for the given type of separator Args: type (str, default=''): one of ``{sep,line,path,ext,alt}``. Returns: separator string. ''' if type in ['path','pathsep']: return os.pathsep elif type in ['ext','extsep']: return os.extsep elif type in ['line','linesep']: return os.linesep elif type in ['alt','altsep']: return os.altsep elif type not in ['','sep']: if not type.endswith('sep'): type += 'sep' print("Error: 'os.%s' not found" % type) raise TypeError return os.sep def minpath(path,pathsep=None): '''remove duplicate paths from given set of paths Args: path (str): path string (e.g. \'/Users/foo/bin:/bin:/sbin:/usr/bin\'). pathsep (str, default=None): path separator (e.g. ``:``). Returns: string composed of one or more paths, with duplicates removed. Examples: >>> minpath(\'.:/Users/foo/bin:.:/Users/foo/bar/bin:/Users/foo/bin\') \'.:/Users/foo/bin:/Users/foo/bar/bin\' ''' if not pathsep: pathsep = os.pathsep pathlist = path.split(pathsep) shortlist = [] for item in pathlist: if item not in shortlist: shortlist.append(item) return pathsep.join(shortlist) #NOTE: broke backward compatibility January 17, 2014 # firstval=False --> all=True # pathDups=True --> minimal=False def env(variable,all=True,minimal=False): '''get dict of environment variables of the form ``{variable:value}`` Args: variable (str): name or partial name for environment variable. all (bool, default=True): if False, only return the first match. minimal (bool, default=False): if True, remove all duplicate paths. Returns: dict of strings of environment variables. Warning: selecting all=False can lead to unexpected matches of *variable*. Examples: >>> env(\'*PATH\') {\'PYTHONPATH\': \'.\', \'PATH\': \'.:/usr/bin:/bin:/usr/sbin:/sbin\'} ''' #better than os.path.expandvars ? import fnmatch vals = {} for key,value in os.environ.items(): if fnmatch.fnmatch(key,variable): vals[key] = value if minimal: for key,value in vals.items(): if fnmatch.fnmatch(key,'*PATH'): vals[key] = minpath(value) if not all: if len(vals) == 0: return return list(vals.values())[0] return vals #NOTE: broke backward compatibility January 17, 2014 # listall --> all def whereis(prog,all=False): #Unix specific (Windows punts to which) '''get path to the given program search the standard binary install locations for the given executable. Args: prog (str): name of an executable to search for (e.g. ``python``). all (bool, default=True): if True, return a list of paths found. Returns: string path of the executable, or list of path strings. ''' if sys.platform[:3] == 'win': return which(prog,all=all) whcom = 'whereis ' p = Popen(whcom+prog, **popen4) p.stdin.close() pathstr = p.stdout.read() p.stdout.close() pathstr = pathstr.decode() paths = pathstr.strip().split(":")[-1] #XXX: ':' ??? -1 ??? pathlist = paths.strip().split() if not pathlist: if not all: pathlist = '' return pathlist if not all: return pathlist[0] return pathlist #NOTE: broke backward compatibility January 17, 2014 # allowlink=True --> allow_links=True # allowerror=False --> ignore_errors=True # listall=False --> all=False def which(prog,allow_links=True,ignore_errors=True,all=False): #Unix specific '''get the path of the given program search the user\'s paths for the given executable. Args: prog (str): name of an executable to search for (e.g. ``python``). allow_links (bool, default=True): if False, replace link with fullpath. ignore_errors (bool, default=True): if True, ignore search errors. all (bool, default=False): if True, get list of paths for executable. Returns: if all=True, get a list of string paths, else return a string path. ''' if sys.platform[:3] == 'win': # try to deal with windows laziness about extensions if not prog.endswith('.exe'): prog += '' if prog.count('.') else '.exe' dirs = env('PATH',all=False) or os.path.abspath(os.curdir) # ? paths = [] _type = None if allow_links else 'file' for _dir in dirs.split(os.pathsep): if all and paths: break paths += find(prog, root=_dir, recurse=False, type=_type) if not all: return paths[0] if len(paths) else '' return paths # non-windows whcom = 'which ' if all: whcom += '-a ' p = Popen(whcom+prog, **popen4) p.stdin.close() pathstr = p.stdout.read() p.stdout.close() errind = 'no '+prog+' in' pathstr = pathstr.decode() if (errind in pathstr) and (ignore_errors): return None pathstr = minpath(pathstr.strip(),os.linesep) paths = pathstr.split(os.linesep) for i in range(len(paths)): if not allow_links and os.path.islink(paths[i]): paths[i] = os.path.realpath(paths[i]) if not all: return paths[0] if len(paths) else '' return paths def find(patterns,root=None,recurse=True,type=None,verbose=False): '''get the path to a file or directory Args: patterns (str): name or partial name of items to search for. root (str, default=None): path of top-level directory to search. recurse (bool, default=True): if True, recurse downward from *root*. type (str, default=None): a search filter. verbose (bool, default=False): if True, be verbose about the search. Returns: a list of string paths. Notes: on some OS, *recursion* can be specified by recursion depth (*int*), and *patterns* can be specified with basic pattern matching. Also, multiple patterns can be specified by splitting patterns with a ``;``. The *type* can be one of ``{file, dir, link, socket, block, char}``. Examples: >>> find(\'pox*\', root=\'..\') [\'/Users/foo/pox/pox\', \'/Users/foo/pox/scripts/pox_launcher.py\'] >>> >>> find(\'*shutils*;*init*\') [\'/Users/foo/pox/pox/shutils.py\', \'/Users/foo/pox/pox/__init__.py\'] ''' if not root: root = os.curdir if type is None: pass elif type in ['f','file']: type = 'f' elif type in ['d','dir']: type = 'd' elif type in ['l','link']: type = 'l' elif type in ['s','socket']: type = 's' elif type in ['b','block']: type = 'b' elif type in ['c','char']: type = 'c' else: if verbose: print("type '%s' not understood, will be ignored" % type) type = None try: if sys.platform[:3] == 'win': raise NotImplementedError pathlist = [] search_list = patterns.split(';') for item in search_list: command = 'find %s -name %r' % (root, item) if type: command += ' -type '+type if not recurse: command += ' -maxdepth 1' elif recurse is not True: command += ' -maxdepth %d' % (int(recurse) + 1) if verbose: print(command) p = Popen(command, **popen4) p.stdin.close() pathstr = p.stdout.readlines() p.stdout.close() errind = ['find:','Usage:'] if errind[1] in pathstr: #XXX: raise error? if verbose: print("Error: incorrect usage '%s'" % command) return for path in pathstr: path = path.decode() if errind[0] not in path: path = path.strip() pathlist.append(os.path.abspath(path)) except: folders = False;files = False;links = False if type in ['f']: files = True elif type in ['l']: links = True elif type in ['d']: folders = True else: folders = True;files = True;links = True pathlist = walk(root,patterns,recurse,folders,files,links) return pathlist # TODO: enable recursion depth def walk(root,patterns='*',recurse=True,folders=False,files=True,links=True): '''walk directory tree and return a list matching the requested pattern Args: root (str): path of top-level directory to search. patterns (str, default=\'*\'): (partial) name of items to search for. recurse (bool, default=True): if True, recurse downward from *root*. folders (bool, default=False): if True, include folders in the results. files (bool, default=True): if True, include files in results. links (bool, default=True): if True, include links in results. Returns: a list of string paths. Notes: patterns can be specified with basic pattern matching. Additionally, multiple patterns can be specified by splitting patterns with a ``;``. Examples: >>> walk(\'..\', patterns=\'pox*\') [\'/Users/foo/pox/pox\', \'/Users/foo/pox/scripts/pox_launcher.py\'] >>> >>> walk(\'.\', patterns=\'*shutils*;*init*\') [\'/Users/foo/pox/pox/shutils.py\', \'/Users/foo/pox/pox/__init__.py\'] ''' import fnmatch #create a list by splitting patterns at ';' pattern_list = patterns.split(';') try: _walk = os.walk except AttributeError: _walk = None #print("walking... ") if _walk: results = [] for dirname,dirs,items in os.walk(root): #followlinks=False if folders or links: for name in dirs: fullname = os.path.normpath(os.path.join(dirname,name)) if(folders and os.path.isdir(fullname) and \ not os.path.islink(fullname)) or \ (links and os.path.islink(fullname)): for pattern in pattern_list: if fnmatch.fnmatch(name,pattern): results.append(fullname) break if files or links: for name in items: fullname = os.path.normpath(os.path.join(dirname,name)) if(files and os.path.isfile(fullname) and \ not os.path.islink(fullname)) or \ (links and os.path.islink(fullname)): for pattern in pattern_list: if fnmatch.fnmatch(name,pattern): results.append(fullname) break #block recursion if disallowed if not recurse: dirs[:] = [] return results #collect arguments into a bunch class Bunch: def __init__(self, **kwds): self.__dict__.update(kwds) arg = Bunch(recurse=recurse,pattern_list=pattern_list, folders=folders, files=files, links=links, results=[]) def visit(arg,dirname,items): #append to arg.results all relevant items for name in items: fullname = os.path.normpath(os.path.join(dirname,name)) if(arg.files and os.path.isfile(fullname) and \ not os.path.islink(fullname)) or \ (arg.folders and os.path.isdir(fullname) and \ not os.path.islink(fullname)) or \ (arg.links and os.path.islink(fullname)): for pattern in arg.pattern_list: if fnmatch.fnmatch(name,pattern): arg.results.append(fullname) break #block recursion if disallowed if not arg.recurse: items[:] = [] os.path.walk(root,visit,arg) # removed in python 3.x return arg.results def where(name,path,pathsep=None): '''get the full path for the given name string on the given search path. Args: name (str): name of file, folder, etc to find. path (str): path string (e.g. \'/Users/foo/bin:/bin:/sbin:/usr/bin\'). pathsep (str, default=None): path separator (e.g. ``:``) Returns: the full path string. Notes: if pathsep is not provided, the OS default will be used. ''' if not pathsep: pathsep = os.pathsep for _path in path.split(pathsep): candidate = os.path.join(_path,name) if os.path.exists(candidate): return os.path.abspath(candidate) return None def mkdir(path,root=None,mode=None): '''create a new directory in the root directory create a directory at *path* and any necessary parents (i.e. ``mkdir -p``). Default mode is read/write/execute for \'user\' and \'group\', and then read/execute otherwise. Args: path (str): string name of the new directory. root (str, default=None): path at which to build the new directory. mode (str, default=None): octal read/write permission [default: 0o775]. Returns: string absolute path for new directory. ''' if mode is None: mode = MODE if not root: root = os.curdir newdir = os.path.join(root,path) absdir = os.path.abspath(newdir) import errno try: os.makedirs(absdir,mode) return absdir except OSError: err = sys.exc_info()[0] if (err.errno != errno.EEXIST) or (not os.path.isdir(absdir)): raise def shellsub(command): '''parse the given command to be formatted for remote shell invocation secure shell (``ssh``) can be used to send and execute commands to remote machines (using ``ssh ``). Additional escape characters are needed to enable the command to be correctly formed and executed remotely. *shellsub* attemps to parse the given command string correctly so that it can be executed remotely with ssh. Args: command (str): the command to be executed remotely. Returns: the parsed command string. ''' import re command = re.compile(r"\'").sub(r"\\'",command) command = re.compile(r'\"').sub(r'\\"',command) command = re.compile(r'\$').sub(r'\\$',command) command = re.compile(r'\(').sub(r'\\(',command) command = re.compile(r'\)').sub(r'\\)',command) #command = re.compile(r'\{').sub(r'\\{',command) #command = re.compile(r'\}').sub(r'\\}',command) #command = re.compile(r'\[').sub(r'\\[',command) #command = re.compile(r'\]').sub(r'\\]',command) #command = re.compile(r'\~').sub(r'\\~',command) #command = re.compile(r'\!').sub(r'\\!',command) #command = re.compile(r'\&').sub(r'\\&',command) #command = re.compile(r'\|').sub(r'\\|',command) #command = re.compile(r'\*').sub(r'\\*',command) #command = re.compile(r'\%').sub(r'\\%',command) #command = re.compile(r'\#').sub(r'\\#',command) #command = re.compile(r'\@').sub(r'\\@',command) #command = re.compile(r'\:').sub(r'\\:',command) #command = re.compile(r'\;').sub(r'\\;',command) #command = re.compile(r'\,').sub(r'\\,',command) #command = re.compile(r'\.').sub(r'\\.',command) #command = re.compile(r'\?').sub(r'\\?',command) #command = re.compile(r'\>').sub(r'\\>',command) #command = re.compile(r'\<').sub(r'\\<',command) #command = re.compile(r'\/').sub(r'\\/',command) #command = re.compile('\\\\').sub('\\\\\\\\',command) return command # backward compatibility getSHELL = shelltype getHOME = homedir getROOT = rootdir getUSER = username getSEP = sep stripDups = minpath if __name__=='__main__': pass # End of file uqfoundation-pox-95e00e4/pox/tests/000077500000000000000000000000001477757307100173615ustar00rootroot00000000000000uqfoundation-pox-95e00e4/pox/tests/__init__.py000066400000000000000000000007341477757307100214760ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2018-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE """ to run this test suite, first build and install `pox`. $ python -m pip install ../.. then run the tests with: $ python -m pox.tests or, if `nose` is installed: $ nosetests """ uqfoundation-pox-95e00e4/pox/tests/__main__.py000066400000000000000000000015071477757307100214560ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2018-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE import glob import os import sys import subprocess as sp import pox shell = sys.platform[:3] == 'win' suite = os.path.dirname(__file__) or os.path.curdir tests = glob.glob(suite + os.path.sep + 'test_*.py') python = pox.which_python(version=True) or sys.executable if __name__ == '__main__': failed = 0 for test in tests: p = sp.Popen([python, test], shell=shell).wait() if p: print('F', end='', flush=True) failed = 1 else: print('.', end='', flush=True) print('') exit(failed) uqfoundation-pox-95e00e4/pox/tests/test_shutils.py000066400000000000000000000074121477757307100224710ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE """ test pox's shell utilities """ import os def test_shutils(): '''script to test all shutils functions''' from pox import shelltype, homedir, rootdir, sep, mkdir, walk, where, env, \ username, minpath, which, which_python, find, shellsub, \ expandvars, __version__ as version #print('testing shelltype...') shell = shelltype() try: assert shell in ['bash','sh','csh','zsh','tcsh','ksh','rc','es','cmd'] except AssertionError: if shell: print("Warning: non-standard shell type") assert isinstance(shell, str) else: print("Warning: could not determine shell type") assert shell is None #print('testing username...') #print(username()) #print('testing homedir...') #print(homedir()) assert homedir().rstrip(sep()).endswith(username()) #print('testing rootdir...') #print(rootdir()) assert homedir().startswith(rootdir()) #print('testing sep...') #print(sep()) #print(sep('ext')) # print(sep('foo')) #print('testing mkdir...') newdir = sep().join(['xxxtest','testxxx']) assert mkdir(newdir).rstrip(sep()).endswith(newdir) #print('cleaning up...') os.removedirs(newdir) #print('testing walk...') #print(walk('/usr/local','*',recurse=False,folders=True,files=False)) folders = walk(rootdir(),'*',recurse=False,folders=True,files=False) assert len(folders) > 0 ### assert all(not os.path.isfile(folder) for folder in folders) home = walk(homedir()+sep()+os.pardir, username(), False, True)[0] assert home == homedir() #print('testing where...') shells = walk(home,'.bashrc',recurse=0) bashrc = where('.bashrc',home) if bashrc: assert bashrc in shells else: assert not shells #print(bashrc) #print('testing minpath...') #print(minpath(os.path.expandvars('$PATH'))) path = expandvars('$PATH') assert minpath(path).count(sep('path')) <= path.count(sep('path')) #print('testing env...') assert env('ACSDAGHQSBFCASDCOMAOCMQOMCQWMOCQOMCOMQRCVOMQOCMQORMCQ') == {} if 'HOME' not in os.environ: os.environ['HOME'] = homedir() assert env('HOME',all=False) or env('USERPROFILE',all=False) == homedir() pathdict = env('*PATH*',minimal=True) assert len(pathdict) > 0 assert all('PATH' in key for key in pathdict) #print('testing which...') python = 'python' if which('python') else 'python3' assert which(python).endswith((python,'python.exe')) assert which(python) in which(python,all=True) #print('testing find...') #print(find('python','/usr/local',type='l')) #print(find('*py;*txt')) x = os.path.dirname(__file__) if not x: # this file is not found x = which('pox;pox_launcher.py') if x: # if executable found, then navigate to the test directory p = which_python(fullpath=False, version=True) x = os.sep.join((x.rsplit(os.sep, 2)[0],'lib',p)) x = [p for p in find('test_shutils.py',x,True,'f') if version in p] x = x[0] if x else '' if x: assert set(find('__init__*;__main__*;test_*',x,False,'f')) == set(find('*py;*pyc',x,recurse=False)) #print('testing shellsub...') command = '${HOME}/bin/which foo("bar")' #print(repr(command)) #print(repr(shellsub(command))) assert shellsub(command) == '\\${HOME}/bin/which foo\\(\\"bar\\"\\)' return if __name__=='__main__': test_shutils() uqfoundation-pox-95e00e4/pox/tests/test_utils.py000066400000000000000000000112551477757307100221360ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE """ test pox's higher-level shell utilities """ import os import sys def test_utils(): '''script to test all utils functions''' from pox import pattern, getvars, expandvars, convert, replace, \ index_join, findpackage, remote, parse_remote, \ select, selectdict, env, homedir, username #print('testing pattern...') assert pattern(['PYTHON*','DEVELOPER']) == 'PYTHON*;DEVELOPER' assert pattern([]) == '' #print('testing getvars...') bogusdict = {'QAZWERFDSXCV_STUFF':'${DV_DIR}/pythia-${QAZWERFDSXCV_VERSION}/stuff', 'MIKE_VERSION':'1.0','MIKE_DIR':'${HOME}/junk', 'DUMMY_VERSION':'6.9','DUMMY_STUFF':'/a/b', 'DV_DIR':'${HOME}/dev', 'QAZWERFDSXCV_VERSION':'0.0.1'} home = homedir() if 'HOME' not in os.environ: os.environ['HOME'] = home assert getvars(home) == {} d1 = {'DV_DIR': '${HOME}/dev', 'QAZWERFDSXCV_VERSION': '0.0.1'} d2 = {'MIKE_DIR': '${HOME}/junk'} assert getvars('${DV_DIR}/pythia-${QAZWERFDSXCV_VERSION}/stuff',bogusdict,'/') == d1 assert getvars('${MIKE_DIR}/stuff',bogusdict,'/') == d2 _home = 'HOME' assert getvars('${%s}/stuff' % _home, sep='/') == {_home: homedir()} #print('testing expandvars...') assert expandvars(home) == homedir() x = '${ASDFQWEGQVQEGQERGQEVQEEEVCQERGWEGWEFGW}/stuff' assert expandvars(x) == x x = '${HOME}/junk/${HOME}/dev/stuff' assert expandvars('${MIKE_DIR}/${DV_DIR}/stuff',bogusdict) == x assert expandvars('${DV_DIR}/${QAZWERFDSXCV_VERSION}',secondref=bogusdict) == \ expandvars('${DV_DIR}/${QAZWERFDSXCV_VERSION}',bogusdict,os.environ) assert expandvars('${%s}/stuff' % _home) == ''.join([homedir(), '/stuff']) #print('testing convert...') source = 'test.txt' f = open(source,'w') f.write('this is a test file.'+os.linesep) f.close() assert convert(source,'mac',verbose=False) == convert(source,verbose=False) assert convert(source,'foo',verbose=False) > 0 #print('testing replace...') replace(source,{' is ':' was '}) replace(source,{'\\sfile.\\s':'.'}) f = open(source,'r') assert f.read().rstrip() == 'this was a test.' f.close() os.remove(source) #print('testing index_join...') fl = ['begin ','hello ','world ','string '] assert index_join(fl,'hello ','world ') == 'hello world ' #print('testing findpackage...') assert not findpackage('python','aoskvaosvoaskvoak',all=True,verbose=False,recurse=False) p = findpackage('lib/python*',env('HOME',all=False),all=False,verbose=False,recurse=1) if p: assert 'lib/python' in p #print('testing remote...') myhost = 'login.cacr.caltech.edu' assert remote('~/dev') == '~/dev' assert 'localhost' in remote('~/dev',loopback=True) thing = '@login.cacr.caltech.edu:~/dev' assert remote('~/dev',host=myhost,user=username()).endswith(thing) #print('testing parse_remote...') destination = 'danse@%s:~/dev' % myhost x = ('-l danse', 'login.cacr.caltech.edu', '~/dev') assert parse_remote(destination,login_flag=True) == x destination = 'danse@%s:' % myhost assert parse_remote(destination) == ('danse', 'login.cacr.caltech.edu', '') destination = '%s:' % myhost x = ('', 'login.cacr.caltech.edu', '') assert parse_remote(destination,login_flag=True) == x destination = 'test.txt' x = ('', 'localhost', 'test.txt') assert parse_remote(destination,loopback=True) == x #print('testing select...') test = ['zero','one','two','three','4','five','six','seven','8','9/81'] assert select(test) == ['three', 'seven'] assert select(test,minimum=True) == ['4', '8'] assert select(test,reverse=True,all=False) == 'seven' assert select(test,counter='/',all=False) == '9/81' test = [[1,2,3],[4,5,6],[1,3,5]] assert select(test) == test assert select(test,counter=3) == [test[0], test[-1]] assert select(test,counter=3,minimum=True) == [test[1]] #print('testing selectdict...') x = {'MIKE_VERSION': '1.0', 'DUMMY_VERSION': '6.9'} assert selectdict(bogusdict,minimum=True) == x x = {'DUMMY_STUFF': '/a/b', 'QAZWERFDSXCV_STUFF': '${DV_DIR}/pythia-${QAZWERFDSXCV_VERSION}/stuff'} assert selectdict(bogusdict,counter='/') == x assert len(selectdict(bogusdict,counter='/',all=False)) == 1 return if __name__=='__main__': test_utils() uqfoundation-pox-95e00e4/pox/utils.py000066400000000000000000000503071477757307100177360ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # # adapted from Mike McKerns' gsl.infect.utils """ higher-level shell utilities for user environment and filesystem exploration """ import os from . import shutils from ._disk import kbytes, disk_used #NOTE: broke backward compatibility January 17, 2014 # seperator --> separator def pattern(list=[],separator=';'): '''generate a filter pattern from list of strings Args: list (list(str), default=[]): a list of filter elements. separator (str, default=';'): the separator string. Returns: a string composed of filter elements joined by the separator. ''' filter = '' for item in list: filter += '%s%s' % (str(item),str(separator)) return filter.rstrip(str(separator)) _varprog = None def expandvars(string,ref=None,secondref={}): """expand shell variables in string Expand shell variables of form ``$var`` and ``${var}``. Unknown variables are left unchanged. If a reference dictionary (*ref*) is provided, restrict lookups to *ref*. A second reference dictionary (*secondref*) can also be provided for failover searches. If *ref* is not provided, lookup variables are defined by the user\'s environment variables. Args: string (str): a string with shell variables. ref (dict(str), default=None): a dict of lookup variables. secondref (dict(str), default={}): a failover reference dict. Returns: string with the selected shell variables substituted. Examples: >>> expandvars(\'found:: $PYTHONPATH\') \'found:: .:/Users/foo/lib/python3.4/site-packages\' >>> >>> expandvars(\'found:: $PYTHONPATH\', ref={}) \'found:: $PYTHONPATH\' """ if ref is None: ref = os.environ refdict = {} refdict.update(secondref) refdict.update(ref) global _varprog if '$' not in string: return string if not _varprog: import re _varprog = re.compile(r'\$(\w+|\{[^}]*\})') i = 0 while True: m = _varprog.search(string, i) if not m: break i, j = m.span(0) name = m.group(1) if name[:1] == '{' and name[-1:] == '}': name = name[1:-1] if name in refdict: tail = string[j:] string = string[:i] + expandvars(refdict[name], ref, secondref) i = len(string) string = string + tail else: i = j return string #NOTE: broke backward compatibility January 17, 2014 # vdict --> ref def getvars(path,ref=None,sep=None): '''get a dictionary of all variables defined in path Extract shell variables of form ``$var`` and ``${var}``. Unknown variables will raise an exception. If a reference dictionary (*ref*) is provided, first try the lookup in *ref*. Failover from *ref* will lookup variables defined in the user\'s environment variables. Use *sep* to override the path separator (``os.sep``). Args: path (str): a path string with shell variables. ref (dict(str), default=None): a dict of lookup variables. sep (str, default=None): the path separator string. Returns: dict of shell variables found in the given path string. Examples: >>> getvars(\'$HOME/stuff\') {\'HOME\': \'/Users/foo\'} ''' #what about using os.path.expandvars ? if ref is None: ref = {} ndict = {} dirs = path.split(sep or os.sep) for dir in dirs: if '$' in dir: key = dir.split('$')[1].lstrip('{').rstrip('}') #get value from ref, or failing... try: ndict[key] = ref[key] #get value from os.environ, or failing... raise a KeyError except KeyError: ndict[key] = os.environ[key] return ndict def convert(files,platform=None,pathsep=None,verbose=True): '''convert text files to given platform type Ensure given files use the appropriate ``os.linesep`` and other formatting. Args: files (list(str)): a list of filenames. platform (str, default=None): platform name as in ``os.name``. pathsep (str, default=None): the path separator string. verbose (bool, default=True): if True, print debug statements.. Returns: 0 if converted, otherwise return 1. ''' if not platform: platform = os.name if not pathsep: pathsep = os.pathsep MAC = '\r' WIN = '\r\n' LIN = '\n' linesep = [WIN,MAC,LIN] #os.name ==> ['posix','nt','os2','mac','ce','riscos'] if platform in ['linux2','linux','unix','lin','posix','riscos']: #??? newlinesep = LIN elif platform in ['windows','mswindows','win','dos','nt','os2','ce']: #??? newlinesep = WIN elif platform in ['mac','macosx']: #??? newlinesep = MAC else: if verbose: print("Error: Platform '%s' not recognized" % platform) return 2 # Error 2: platform not recognized allconverted = 0 # Success for file in files.split(pathsep): try: infile = open(file, 'r') filestring = infile.read() infile.close() for i in linesep: filestring = filestring.replace(i, newlinesep) outfile = open(file, 'w') outfile.write(filestring) outfile.close() if verbose: print("Converted '%s' to '%s' format" % (file,platform)) except: if verbose: print("File conversion failed for '%s'" % (file)) allconverted = 1 # Error 1: file conversion failed return allconverted def replace(file,sub={},outfile=None): '''make text substitutions given by *sub* in the given file Args: file (str): path to original file. sub (dict(str), default={}: dict of string replacements ``{old:new}``. outfile (str, default=None): if given, don't overwrite original file. Returns: None Notes: ``replace`` uses regular expressions, thus a pattern may be used as *old* text. ``replace`` can fail if order of substitution is important. ''' #XXX: use OrderedDict instead... would enable ordered substitutions if outfile == None: outfile = file input = open(file, 'r') filestring = input.read() input.close() import re for old,new in sub.items(): filestring = re.compile(old).sub(new,filestring) output = open(outfile, 'w') output.write(filestring) output.close() return def index_slice(sequence,start,stop,step=1,sequential=False,inclusive=False): '''get the slice for a given sequence Slice indicies are determined by the positions of *start* and *stop*. If *start* is not found in the sequence, slice from the beginning. If *stop* is not found in the sequence, slice to the end. Args: sequence (list): an ordered sequence of elements. start (int): index for start of the slice. stop (int): index for stop position in the sequence. step (int, default=1): indices until next member of the slice. sequential (bool, default=False): if True, *start* must preceed *stop*. inclusive (bool, default=False): if True, include *stop* in the slice. Returns: slice corresponding to given *start*, *stop*, and *step*. ''' if start in sequence: begin = sequence.index(start) else: begin = None if sequential: here = None else: here = begin if stop in sequence[here:]: end = sequence[here:].index(stop) if here: end += here else: end = None if inclusive and end != None: end += 1 return slice(begin,end,step) def index_join(sequence,start,stop,step=1,sequential=True,inclusive=True): '''slice a list of strings, then join the remaining strings If *start* is not found in the sequence, slice from the beginning. If *stop* is not found in the sequence, slice to the end. Args: sequence (list): an ordered sequence of elements. start (int): index for start of the slice. stop (int): index for stop position in the sequence. step (int, default=1): indices until next member of the slice. sequential (bool, default=True): if True, *start* must preceed *stop*. inclusive (bool, default=True): if True, include *stop* in the slice. Returns: string produced by slicing the given sequence and joining the elements. ''' islice = index_slice(sequence,start,stop,step,sequential,inclusive) return ''.join(sequence[islice]) #NOTE: broke backward compatibility January 17, 2014 # firstval=False --> all=False def findpackage(package,root=None,all=False,verbose=True,recurse=True): '''retrieve the path(s) for a package Args: package (str): name of the package to search for. root (str, default=None): path string of top-level directory to search. all (bool, defualt=False): if True, return everywhere package is found. verbose (bool, default=True): if True, print messages about the search. recurse (bool, default=True): if True, recurse down the root directory. Returns: string path (or list of paths) where package is found. Notes: On some OS, recursion can be specified by recursion depth (an integer). ``findpackage`` will do standard pattern matching for package names, attempting to match the head directory of the distribution. ''' if not root: root = os.curdir if verbose: print('searching %s...' % root) if package[0] != os.sep: package = os.sep+package packdir,basedir = os.path.split(package) targetdir = shutils.find(basedir,root,recurse=recurse,type='d') #print("targetdir: "+targetdir) #remove invalid candidate directories (and 'BLD_ROOT' & 'EXPORT_ROOT') bldroot = shutils.env('BLD_ROOT',all=False) exproot = shutils.env('EXPORT_ROOT',all=False) remlist = [] import fnmatch for dir in targetdir: if (not fnmatch.fnmatch(dir,'*'+package)) or \ (not fnmatch.fnmatch(os.path.basename(dir),basedir)) or \ ((bldroot) and (bldroot in dir)) or \ ((exproot) and (exproot in dir)): remlist.append(dir) #build list of bad matches for dir in remlist: targetdir.remove(dir) if verbose: if targetdir: print('%s found' % package) else: print('%s not found' % package) if all: return targetdir return select(targetdir,counter=os.sep,minimum=True,all=False) #NOTE: broke backward compatibility January 18, 2014 # minimum=True --> minimum=False def select(iterable,counter='',minimum=False,reverse=False,all=True): '''find items in iterable with the max (or min) count of the given counter. Find the items in an iterable that have the maximum number of *counter* (e.g. counter=\'3\' counts occurances of \'3\'). Use ``minimum=True`` to search for the minimum number of occurances of the *counter*. Args: iterable (list): an iterable of iterables (e.g. lists, strings, etc). counter (str, default=''): the item to count. minimum (bool, default=False): if True, find min count (else, max). reverse (bool, default=False): if True, reverse order of the results. all (bool, default=True): if False, only return the first result. Returns: list of items in the iterable with the min (or max) count. Examples: >>> z = [\'zero\',\'one\',\'two\',\'three\',\'4\',\'five\',\'six\',\'seven\',\'8\',\'9/81\'] >>> select(z, counter=\'e\') [\'three\', \'seven\'] >>> select(z, counter=\'e\', minimum=True) [\'two\', \'4\', \'six\', \'8\', \'9/81\'] >>> >>> y = [[1,2,3],[4,5,6],[1,3,5]] >>> select(y, counter=3) [[1, 2, 3], [1, 3, 5]] >>> select(y, counter=3, minumim=True, all=False) [4, 5, 6] ''' itype = type(iterable) m = [] for item in iterable: try: count = item.count(counter) except TypeError: count = 0 # catches '33'.count(3) --> 0 except AttributeError: # catches 33.count(3) --> 0 (or 1) count = 1 if item == counter else 0 m.append(count) if reverse: m.reverse() iterable.reverse() if not m: if all == True: return itype([]) else: return None if minimum: x = min(m) else: x = max(m) if not all: return iterable[m.index(x)] shortlist = [] tmp = [] for item in iterable: tmp.append(item) occurances = m.count(x) for i in range(occurances): shortlist.append(tmp.pop(m.index(x))) m.pop(m.index(x)) return itype(shortlist) #NOTE: broke backward compatibility January 18, 2014 # minimum=True --> minimum=False def selectdict(dict,counter='',minimum=False,all=True): '''return a dict of items with the max (or min) count of the given counter. Get the items from a dict that have the maximum number of the *counter* (e.g. counter=\'3\' counts occurances of \'3\') in the values. Use ``minimum=True`` to search for minimum number of occurances of *counter*. Args: dict (dict): dict with iterables as values (e.g. lists, strings, etc). counter (str, default=''): the item to count. minimum (bool, default=False): if True, find min count (else, max). all (bool, default=True): if False, only return the first result. Returns: dict of items composed of the entries with the min (or max) count. Examples: >>> z = [\'zero\',\'one\',\'two\',\'three\',\'4\',\'five\',\'six\',\'seven\',\'8\',\'9/81\'] >>> z = dict(enumerate(z)) >>> selectdict(z, counter=\'e\') {3: \'three\', 7: \'seven\'} >>> selectdict(z, counter=\'e\', minimum=True) {8: \'8\', 9: \'9/81\', 2: \'two\', 4: \'4\', 6: \'six\'} >>> >>> y = {1: [1,2,3], 2: [4,5,6], 3: [1,3,5]} >>> selectdict(y, counter=3) {1: [1, 2, 3], 3: [1, 3, 5]} >>> selectdict(y, counter=3, minumim=True) {2: [4, 5, 6]} ''' keys,values = zip(*dict.items()) shortlist = select(values,counter,minimum,all=all) if not all: x = list(values).index(shortlist) return {keys[x]: values[x]} shortdict = {} for i in range(len(values)): if values[i] in shortlist: shortdict.update({keys[i]: values[i]}) return shortdict #NOTE: broke backward compatibility January 18, 2014 # forceSSH --> loopback def remote(path,host=None,user=None,loopback=False): '''build string for a remote connection of the form ``[[user@]host:]path`` Args: path (str): path string for location of target on (remote) filesystem. host (str, default=None): string name/ip address of (remote) host. user (str, default=None): user name on (remote) host. loopback (bool, default=False): if True, ensure *host* is used. Returns: a remote connection string. Notes: if loopback=True and host=None, then host will be set to localhost. ''' if loopback and not host: host = 'localhost' if host: path = '%s:%s' % (host,path) if user: path = '%s@%s' % (user,path) return path #NOTE: broke backward compatibility January 18, 2014 # forceSSH --> loopback # useOption --> login_flag def parse_remote(path,loopback=False,login_flag=False): """parse remote connection string of the form ``[[user@]host:]path`` Args: path (str): remote connection string. loopback (bool, default=False): if True, ensure *host* is used. login_flag (bool, default=False): if True, prepend user with ``-l``. Returns: a tuple of the form ``(user, host, path)``. Notes: if loopback=True and host=None, then host will be set to localhost. """ dpath = path.split(':')[-1] rhost = path.split(':')[0] if rhost == dpath: if loopback: return '','localhost',dpath return '','',dpath dhost = rhost.split('@')[-1] duser = rhost.split('@')[0] if duser == dhost: return '',dhost,dpath if login_flag: duser = '-l '+duser return duser,dhost,dpath def which_python(version=False, lazy=False, fullpath=True, ignore_errors=True): """get the command to launch the selected version of python ``which_python`` composes a command string that can be used to launch the desired python executable. The user\'s path is searched for the executable, unless ``lazy=True`` and thus only a lazy-evaluating command (e.g. ``which python``) is produced. Args: version (bool, default=False): if True, include the version of python. lazy (bool, default=False): if True, build a lazy-evaluating command. fullpath (bool, default=True): if True, provide the full path. ignore_errors (bool, default=True): if True, ignore path search errors. Returns: string of the implicit or explicit location of the python executable. Notes: if version is given as an int or float, include the version number in the command string. if the executable is not found, an error will be thrown unless ``ignore_error=True``. """ import os import sys base = os.path.basename(sys.executable) IS_PYPY = base.startswith('pypy') target = "pypy" if IS_PYPY else "python"; tail = "" if lazy and not (sys.platform[:3] == 'win'): target = "`which {0}".format(target); tail = "`" # include version number DOT = "." if (not IS_PYPY) or ("." in base) else "" if str(version).startswith(('2','3','4','5','6','7','8','9','1','0')): if "." in str(version): pyversion = str(version) if DOT else "".join(str(version).split('.')) else: pyversion = str(float(version)) if "-" in base else str(version) elif bool(version): pyversion = DOT.join(str(i) for i in sys.version_info[0:2]) else: pyversion = "" DASH = "-" if (pyversion and "-" in base) else "" target = "".join([DASH.join([target, pyversion]), tail]) # lookup full path if not lazy and fullpath: #XXX: better to use 'version' kwd directly...? version = pyversion.split('.') sysversion = sys.version_info[:len(version)] #XXX: <= 10 if not pyversion or tuple(int(i) for i in version) == sysversion: target = sys.executable else: target = shutils.which(target, ignore_errors=True) if not target: target = None #XXX: better None or "" ? return target def wait_for(path,sleep=1,tries=150,ignore_errors=False): """block execution by waiting for a file to appear at the given path Args: path (str): the path string to watch for the file. sleep (float, default=1): the time between checking results. tries (int, default=150): the number of times to try. ignore_errors (bool, default=False): if True, ignore timeout error. Returns: None Notes: if the file is not found after the given number of tries, an error will be thrown unless ``ignore_error=True``. using ``subproc = Popen(...)`` and ``subproc.wait()`` is usually a better approach. However, when a handle to the subprocess is unavailable, waiting for a file to appear at a given path is a decent last resort. """ from subprocess import call maxcount = int(tries); counter = 0 sync = shutils.which('sync', all=False) while not os.path.exists(path): if sync: call('sync', shell=True) import time # wait for results time.sleep(sleep); counter += 1 if counter >= maxcount: if not ignore_errors: raise IOError("%s not found" % path) print("Warning: exceeded timeout (%s tries)" % maxcount) break return # backward compatability makefilter = pattern getVars = getvars replaceText = replace getLines = index_join makeTarget = remote parseTarget = parse_remote prunelist = select prunedict = selectdict if __name__=='__main__': pass # End of file uqfoundation-pox-95e00e4/pyproject.toml000066400000000000000000000002461477757307100203270ustar00rootroot00000000000000[build-system] # Further build requirements come from setup.py via the PEP 517 interface requires = [ "setuptools>=42", ] build-backend = "setuptools.build_meta" uqfoundation-pox-95e00e4/scripts/000077500000000000000000000000001477757307100171005ustar00rootroot00000000000000uqfoundation-pox-95e00e4/scripts/pox000066400000000000000000000012641477757307100176340ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2018-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE import pox.__main__ from pox.__main__ import * __doc__ = pox.__main__.__doc__ if __name__=='__main__': import sys try: func = sys.argv[1] except: func = None if func: try: exec('print(%s)' % func) except: print("Error: incorrect syntax '%s'\n" % func) exec('print(%s.__doc__)' % func.split('(')[0]) else: help() # End of file uqfoundation-pox-95e00e4/setup.cfg000066400000000000000000000001761477757307100172360ustar00rootroot00000000000000[egg_info] #tag_build = .dev0 [bdist_wheel] #python-tag = py3 #plat-name = manylinux_2_28_x86_64 [sdist] #formats=zip,gztar uqfoundation-pox-95e00e4/setup.py000066400000000000000000000076101477757307100171270ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE import os import sys # drop support for older python if sys.version_info < (3, 8): unsupported = 'Versions of Python before 3.8 are not supported' raise ValueError(unsupported) # get distribution meta info here = os.path.abspath(os.path.dirname(__file__)) sys.path.append(here) from version import (__version__, __author__, __contact__ as AUTHOR_EMAIL, get_license_text, get_readme_as_rst, write_info_file) LICENSE = get_license_text(os.path.join(here, 'LICENSE')) README = get_readme_as_rst(os.path.join(here, 'README.md')) # write meta info file write_info_file(here, 'pox', doc=README, license=LICENSE, version=__version__, author=__author__) del here, get_license_text, get_readme_as_rst, write_info_file # check if setuptools is available try: from setuptools import setup from setuptools.dist import Distribution has_setuptools = True except ImportError: from distutils.core import setup Distribution = object has_setuptools = False # build the 'setup' call setup_kwds = dict( name='pox', version=__version__, description='utilities for filesystem exploration and automated builds', long_description = README.strip(), author = __author__, author_email = AUTHOR_EMAIL, maintainer = __author__, maintainer_email = AUTHOR_EMAIL, license = 'BSD-3-Clause', platforms = ['Linux', 'Windows', 'Mac'], url = 'https://github.com/uqfoundation/pox', download_url = 'https://pypi.org/project/pox/#files', project_urls = { 'Documentation':'http://pox.rtfd.io', 'Source Code':'https://github.com/uqfoundation/pox', 'Bug Tracker':'https://github.com/uqfoundation/pox/issues', }, python_requires = '>=3.8', classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Scientific/Engineering', 'Topic :: Software Development', ], packages = ['pox','pox.tests'], package_dir = {'pox':'pox','pox.tests':'pox/tests'}, scripts=['scripts/pox'], ) # force python-, abi-, and platform-specific naming of bdist_wheel class BinaryDistribution(Distribution): """Distribution which forces a binary package with platform name""" def has_ext_modules(foo): return True # define dependencies dummy_version = 'dummy>=0.1' extra_version = 'extra>=0.1' # add dependencies depend = [dummy_version] extras = {'extra': [extra_version]} # update setup kwds if has_setuptools: setup_kwds.update( zip_safe=False, # distclass=BinaryDistribution, # install_requires=depend, # extras_require=extras, ) # call setup setup(**setup_kwds) # if dependencies are missing, print a warning try: pass except ImportError: print("\n***********************************************************") print("WARNING: One of the following dependencies is unresolved:") # print(" %s" % dummy_version) print("***********************************************************\n") uqfoundation-pox-95e00e4/tools/000077500000000000000000000000001477757307100165515ustar00rootroot00000000000000uqfoundation-pox-95e00e4/tools/README000066400000000000000000000022031477757307100174260ustar00rootroot00000000000000# scripts to augment the user's environment for distrubted computing == bash_profile ============ FEATURES: - default entry point for pathos distributed communications - activates enviroment aliases and functions provided by other files INSTALLATION: - copy or append bash_profile to $HOME/.bash_profile == pathosrc ============ FEATURES: - provide user-owned 'bin' and 'lib' depositories - activate enhancents provided by .python - extend pythonpath to user's enviroment INSTALLATION: - copy pathosrc to $HOME/.pathosrc == pythonstartup ============ FEATURES: - provides python interpreter with 'tab' completion - provides python interpreter with 'scroll" access to history across sessions INSTALLATION: - copy pythonstartup to $HOME/.python == ssh_agent ============ FEATURES: - provides ssh-keypair authentication - facilitates automated authentication to 'known_hosts' INSTALLATION: - copy ssh_agent to $HOME/.ssh_agent == ssh_config ============ FEATURES: - provides ssh-agent forwarding - extends automated authentication to single-point authentication INSTALLATION: - copy ssh_config to $HOME/.ssh/config # EOF uqfoundation-pox-95e00e4/tools/bash_profile000066400000000000000000000011161477757307100211300ustar00rootroot00000000000000# .bash_profile (additions) # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # Get standard aliases and functions if [ -f ~/.bashrc ]; then . ~/.bashrc fi # Activate the ssh-agent if [ -f ~/.ssh_agent ]; then . ~/.ssh_agent fi # Get additional aliases and functions if [ -f ~/.pathosrc ]; then . ~/.pathosrc fi uqfoundation-pox-95e00e4/tools/pathosrc000066400000000000000000000014051477757307100203170ustar00rootroot00000000000000# .pathosrc # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # user environment PATH=$PATH:.:$HOME/bin LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.:$HOME/lib MYPYTHON = $HOME/python if [ "$PYTHONPATH" = "" ] then PYTHONPATH=.:$MYPYTHON else PYTHONPATH=.:$MYPYTHON:$PYTHONPATH fi export PATH export LD_LIBRARY_PATH export PYTHONPATH export PYTHONSTARTUP=$HOME/.python export PYTHONHISTORY=$HOME/.pyhistory unset USERNAME # user aliases alias pythonpath='python -c "import sys; print(sys.path)"' # EOF uqfoundation-pox-95e00e4/tools/pythonstartup000066400000000000000000000034111477757307100214370ustar00rootroot00000000000000#!/usr/bin/env python # # Provides readline tools, including: # - 'tab' completion # - 'scroll' access to history across python sessions # # to activate, run the following in your command shell: # export PYTHONSTARTUP=$HOME/.python # export PYTHONHISTORY=$HOME/.pyhistory # cp ./pythonstartup $PYTHONSTARTUP # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE try: import readline except ImportError: print ("Module readline not available.") else: import rlcompleter if readline.__doc__ and 'libedit' in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete") import os import atexit import sys _version = 1 # {0: 'all-in-one', 1: 'different-major', 2: 'different-minor'} _version = ".".join(str(i) for i in sys.version_info[:_version]) try: historyPath = os.environ["PYTHONHISTORY"] + _version except KeyError: historyPath = os.path.expanduser("~"+os.sep+".pyhistory" + _version) print ("Imported history from: '%s'" % historyPath) print ("Set history file with PYTHONHISTORY.") else: pass try: if os.path.exists(historyPath): readline.read_history_file(historyPath) except IOError: print ("Error reading: '%s'" % historyPath) print ("Skipping history.") def save_history(historyPath=historyPath): import readline readline.write_history_file(historyPath) atexit.register(save_history) del _version del os, sys, atexit, readline, rlcompleter, save_history, historyPath uqfoundation-pox-95e00e4/tools/ssh_agent000066400000000000000000000015211477757307100204460ustar00rootroot00000000000000# .ssh_agent # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE # --- ssh-agent --- # If this shell is interactive, restart it within an ssh-agent. This # depends on ssh-agent and ssh-add being on the user's default PATH, which # should be the case if they're in /usr/bin where they should be. case $- in *i*) if [ "$SSH_AUTH_SOCK" == "" ] ; then exec ssh-agent $SHELL -l$- $* fi case "`ssh-add -l`" in "The agent has no identities."*) for i in identity id_rsa ; do if [ -r .ssh/$i ] ; then ssh-add .ssh/$i fi ; done esac esac # --- end ssh-agent --- uqfoundation-pox-95e00e4/tools/ssh_config000066400000000000000000000006121477757307100206150ustar00rootroot00000000000000Host * ForwardAgent yes # ForwardX11 no # ForwardX11Trusted no # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 1997-2016 California Institute of Technology. # Copyright (c) 2016-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE uqfoundation-pox-95e00e4/tox.ini000066400000000000000000000005701477757307100167260ustar00rootroot00000000000000[tox] skip_missing_interpreters= True envlist = py38 py39 py310 py311 py312 py313 py314 pypy38 pypy39 pypy310 [testenv] deps = # pytest>=3.0.0 whitelist_externals = # bash commands = {envpython} -m pip install . {envpython} pox/tests/__main__.py # pytest --cache-clear \ # --ignore=pox/tests/test_shutils.py uqfoundation-pox-95e00e4/version.py000066400000000000000000000061471477757307100174600ustar00rootroot00000000000000#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2022-2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/pox/blob/master/LICENSE __version__ = '0.3.6'#.dev0' __author__ = 'Mike McKerns' __contact__ = 'mmckerns@uqfoundation.org' def get_license_text(filepath): "open the LICENSE file and read the contents" try: LICENSE = open(filepath).read() except: LICENSE = '' return LICENSE def get_readme_as_rst(filepath): "open the README file and read the markdown as rst" try: fh = open(filepath) name, null = fh.readline().rstrip(), fh.readline() tag, null = fh.readline(), fh.readline() tag = "%s: %s" % (name, tag) split = '-'*(len(tag)-1)+'\n' README = ''.join((null,split,tag,split,'\n')) skip = False for line in fh: if line.startswith('['): continue elif skip and line.startswith(' http'): README += '\n' + line elif line.startswith('* '): README += line.replace('* ',' - ',1) elif line.startswith('-'): README += line.replace('-','=') + '\n' elif line.startswith('!['): # image alt,img = line.split('](',1) if img.startswith('docs'): # relative path img = img.split('docs/source/',1)[-1] # make is in docs README += '.. image:: ' + img.replace(')','') README += ' :alt: ' + alt.replace('![','') + '\n' #elif ')[http' in line: # alt text link (`text `_) else: README += line skip = line.endswith(':\n') fh.close() except: README = '' return README def write_info_file(dirpath, modulename, **info): """write the given info to 'modulename/__info__.py' info expects: doc: the module's long_description version: the module's version string author: the module's author string license: the module's license contents """ import os infofile = os.path.join(dirpath, '%s/__info__.py' % modulename) header = '''#!/usr/bin/env python # # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) # Copyright (c) 2025 The Uncertainty Quantification Foundation. # License: 3-clause BSD. The full license text is available at: # - https://github.com/uqfoundation/%s/blob/master/LICENSE ''' % modulename #XXX: author and email are hardwired in the header doc = info.get('doc', None) version = info.get('version', None) author = info.get('author', None) license = info.get('license', None) with open(infofile, 'w') as fh: fh.write(header) if doc is not None: fh.write("'''%s'''\n\n" % doc) if version is not None: fh.write("__version__ = %r\n" % version) if author is not None: fh.write("__author__ = %r\n\n" % author) if license is not None: fh.write("__license__ = '''\n%s'''\n" % license) return