[
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 88\nimport-order-style = appnexus\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: gist continuous integration\n\non: [pull_request, push]\n\njobs:\n  build:\n    strategy:\n      max-parallel: 16\n      matrix:\n        python-version: [3.6, 3.7, 3.8, 3.9]\n        os: [macos-latest, ubuntu-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        python -m pip install poetry\n        poetry install\n    - name: Lint\n      run: |\n        make lint\n    - name: Test\n      run: |\n        make test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nvenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n*.eggs\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\ninstalled-files.txt\nbin/gistc\ntests/gnupg/*\n\n.vscode\n.envrc\n.python-version\n.vimlocal\ntags\n.venv/\n.poetry-install-run\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Joshua Downer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL=/bin/bash\n\nTEST_FILES:=$(wildcard tests/*.py)\nSRC_FILES:=$(wildcard gist/*.py)\n\nREQ_BINS := poetry\n$(foreach bin,$(REQ_BINS),\\\n    $(if $(shell which $(bin) 2> /dev/null),,$(error Missing required package `$(bin)`)))\n\n.PHONY: build test lint tox clean export\n\n.poetry-install-run:\n\t@poetry install --remove-untracked\n\t@touch .poetry-install-run\n\nbuild:\n\t@poetry build\n\ntest: .poetry-install-run\n\t@poetry run pytest --ff -x -v -s tests\n\nlint: .poetry-install-run\n\t@poetry run black --quiet --check $(TEST_FILES) $(SRC_FILES)\n\t@poetry run flake8 $(TEST_FILES) $(SRC_FILES)\n\ntox: .poetry-install-run\n\t@poetry run tox\n\nclean:\n\tgit clean -xdf\n\nexport:\n\t@poetry export --without-hashes -o requirements.txt\n\t@poetry export --dev --without-hashes -o requirements-dev.txt\n"
  },
  {
    "path": "README.rst",
    "content": "==================================================\nGIST\n==================================================\n\n'gist' is a command line interface for working with github gists. It provides\nseveral methods for inspecting a users gists, and the ability to easily create\nthem.\n\n.. image:: https://github.com/jdowner/gist/workflows/gist%20continuous%20integration/badge.svg\n    :target: https://github.com/jdowner/gist\n\n\nRequirements\n--------------------------------------------------\nPython 3.6, 3.7, 3.8, or 3.9 is required.\n\n\nInstallation\n--------------------------------------------------\n\nThe preferred way to install 'gist' is from pypi.org using pip (or pip3),\n\n::\n\n  $ pip install python-gist\n\nAlternatively, you can clone the repository and install it manually,\n\n::\n\n  $ pip install .\n\nThe 'share' directory contains a set of shell scripts that provide tab\ncompletion and fuzzy search for gist. There are 3 different scripts for\ntab-completion in bash: gist.bash, gist-fzf.bash, and gist-fzsl.bash. The first\nprovides simple tab completion and can be enable by adding the following to\nyour .bashrc file,\n\n::\n\n  source /usr/local/share/gist/gist.bash\n\nThe other scripts, gist-fzf.bash and fist-fzsl.bash, provide fuzzy matching of\ngists using an ncurses interface (NB: these scripts require\n`fzf <https://github.com/junegunn/fzf>`_ and `fzsl <https://github.com/jsbronder/fzsl>`_,\nrespectively).\n\nThe gist.fish script provides tab completion for the fish shell, and should be\ncopied to ~/.config/fish/completions.\n\nThe gist.zsh script provides tab completion for the zsh shell, and should be\ncopied to ~/.zsh as _gist. If not already in your ~/.zshrc file, you should add\n\n::\n\n  fpath=(${HOME}/.zsh $fpath)\n\nTo check that 'gist' is operating correctly, you can run the unit tests with,\n\n::\n\n  $ make test\n\nNote that running the unit tests requires `poetry <https://python-poetry.org/>`_\nto be available on your PATH.\n\n\nGetting started\n--------------------------------------------------\n\n'gist' requires a personal access token for authentication. To create a token,\ngo to https://github.com/settings/tokens. The token needs to then be added\nto a 'gist' configuration file that should have the form,\n\n::\n\n  [gist]\n  token: <enter token here>\n  editor: <path to editor>\n\nThe editor field is optional. If the default editor is specified through some\nother mechanism 'gist' will try to infer it. Otherwise, you can use the config\nfile to ensure that 'gist' uses the editor you want it to use.\n\nIf the token string begins with ``!`` the text following is interpreted as a\nshell command which, when executed, prints the token to stdout. For example::\n\n  [gist]\n  token: !gpg --decrypt github-token.gpg\n\nThe configuration file must be in one of the following,\n\n::\n\n  ${XDG_DATA_HOME}/gist\n  ${HOME}/.config/gist\n  ${HOME}/.gist\n\nIf more than one of these files exist, this is also the order of preference,\ni.e. a configuration that is found in the ``${XDG_DATA_HOME}`` directory will be\ntaken in preference to ``${HOME}/.config/gist``.\n\nAlso, 'gist' assumes that you have set up your github account to use SSH keys so\nthat you can access your repositories without needing to provide a password.\nHere__ is a link on setting up SSH keys with github.\n\n__ https://help.github.com/articles/connecting-to-github-with-ssh/\n\n\nUsage\n--------------------------------------------------\n\n'gist' is intended to make it easy to manage and use github gists from the\ncommand line. There are several commands available:\n\n::\n\n  gist create      - creates a new gist\n  gist edit        - edit the files in your gist\n  gist description - updates the description of your gist\n  gist list        - prints a list of your gists\n  gist clone       - clones a gist\n  gist delete      - deletes a gist or list of gists from github\n  gist files       - prints a list of the files in a gist\n  gist archive     - downloads a gist and creates a tarball\n  gist content     - prints the content of the gist to stdout\n  gist info        - prints detailed information about a gist\n  gist version     - prints the current version\n  gist help        - prints the help documentation\n\n\n**gist create**\n\nMost of the 'gist' commands are pretty simple and limited in what they can do.\n'gist create' is a little different and offers more flexibility in how the user\ncan create the gist.\n\nIf you have a set of existing files that you want to turn into a gist,\n\n::\n\n  $ gist create \"divide et impera\" foo.txt bar.txt\n\nwhere the quoted string is the description of the gist. Or, you may find it\nuseful to create a gist from content on your clipboard (say, using xclip),\n\n::\n\n  $ xclip -o | gist create \"ipsa scientia potestas est\"\n\nAnother option is to pipe the input into 'gist create' and have it automatically\nput the content on github,\n\n::\n\n  $ echo $(cat) | gist create \"credo quia absurdum est\"\n\nFinally, you can just call,\n\n::\n\n  $ gist create \"a posse ad esse\"\n\nwhich will launch your default editor (defined by the EDITOR environment\nvariable).\n\nIn addition to creating gists using the above methods, it is also possible to\nencrypt a gist if you have gnupg installed. Any of the above methods can be used\nto create encrypted gists by simply adding the --encrypt flag to invocation.\nFor example,\n\n::\n\n  $ gist create \"arcana imperii\" --encrypt\n\nwill open the editor allowing you to create the content of the gist, which is\nthen encrypted and added to github. See the Configuration section for\ninformation on how to enable gnupg support.\n\n\n**gist edit**\n\nYou can edit your gists directly with the 'edit' command. This command will\nclone the gist to a temporary directory and open up the default editor (defined\nby the EDITOR environment variable) to edit the files in the gist. When the\neditor is exited the user is prompted to commit the changes, which are then\npushed back to the remote.\n\n**gist description**\n\nYou can update the description of your gist with the 'description' command.\nYou need to supply the gist ID and the new description. For example -\n\n::\n\n  $ gist description e1f5e95a1705cbfde144 \"This is a new description\"\n\n\n**gist list**\n\nReturns a list of your gists. The gists are returned as,\n\n::\n\n  2b1823252e8433ef8682 - mathematical divagations\n  a485ee9ddf6828d697be - notes on defenestration\n  589071c7a02b1823252e + abecedarian pericombobulations\n\nThe first column is the gists unique identifier; The second column indicates\nwhether the gist is public ('+') or private ('-'); The third column is the\ndescription in the gist, which may be empty.\n\n\n**gist clone**\n\nClones a gist to the current directory. This command will clone any gist based\non its unique identifier (i.e. not just the users) to the current directory.\n\n\n**gist delete**\n\nDeletes the specified gists from github.\n\n\n**gist files**\n\nReturns a list of the files in the specified gist.\n\n\n**gist archive**\n\nDownloads the specified gist to a temporary directory and adds it to a tarball,\nwhich is then moved to the current directory.\n\n\n**gist content**\n\nWrites the content of each file in the specified gist to the terminal, e.g.\n\n::\n\n  $ gist content c971fca7997aed65ddc9\n  foo.txt:\n  this is foo\n\n\n  bar.txt:\n  this is bar\n\n\nFor each file in the gist the first line is the name of the file followed by a\ncolon, and then the content of that file is written to the terminal.\n\nIf a filename is given, only the content of the specified filename will be\nprinted.\n\n::\n\n  $ gist content de42344a4ecb6250d6cea00d9da6d83a file1\n  content of file 1\n\n\nIf the contents of the gist is encrypted, it can be viewed in its decrypted\nform by adding the --decrypt flag, e.g.\n\n::\n\n  $ gist content --decrypt 8fe557fb3771aa74edfd\n  foo.txt.asc (decrypted):\n  this is a secret\n\n\nSee the Configuration section for information on how to enable gnupg support.\n\n\n**gist info**\n\nThis command provides a complete dump of the information about the gist as a\nJSON object. It is mostly useful for debugging.\n\n\n**gist version**\n\nSimply prints the current version.\n\n\n**gist help**\n\nPrints out the help documentation.\n\n\nConfiguration\n--------------------------------------------------\n\nThere are several parameters that can be added to a configuration file to\ndetermine the behavior of gist. The configuration file itself is expected to\nbe one of the following paths,\n\n::\n\n  ${HOME}/.gist\n  ${HOME}/.config/gist\n  ${XDG_DATA_HOME}/gist\n\nThe configuration file follows the .ini style. The following is an example,\n\n::\n\n  [gist]\n  token: dde7b84d1e0edf7454ab354934b6ab36b01bf00f\n  editor: /usr/bin/vim\n  gnupg-homedir: /home/user/.gnupg\n  gnupg-fingerprint: 179F9650D9FC1BFE391620B4B13A7829D8DE8623\n  delete-tempfiles: False\n\nThe only essential field in the configuration file is the token. This is the\nauthentication token from github that grants gist permission to access your\ngists. The editor is the editor to use if the EDITOR environment is not set or\nyou wish to use a different editor. 'gnupg-homedir' is the directory where your\ngnupg data are stored, and 'gnupg-fingerprint' is the fingerprint of the key to\nuse to encrypt data in your gists. Both gnupg fields are required to support\nencryption/decryption.\n\nThe 'delete-tempfiles' option is used when gists are created from an editor.\nThe editor writes its contents to a temporary file, which is deleted by\ndefault. The default behavior can be overridden by using the 'delete-tempfiles'\nflag.\n\n\nContributors\n--------------------------------------------------\n\nThank you to the following people for contributing to 'gist'!\n\n* Eren Inan Canpolat (https://github.com/canpolat)\n* Kaan Genç (https://github.com/SeriousBug)\n* Eric James Michael Ritz (https://github.com/ejmr)\n* Karan Parikh (https://github.com/karanparikh)\n* Konstantin Krastev (https://github.com/grizmin)\n* Brandon Davidson (https://github.com/brandond)\n* jq170727 (https://github.com/jq170727)\n* jsbronder (https://github.com/jsbronder)\n* hugsy (https://github.com/hugsy)\n* Kenneth Benzie (https://github.com/kbenzie)\n* rtfmoz2 (https://github.com/rtfmoz2)\n"
  },
  {
    "path": "gist/__init__.py",
    "content": ""
  },
  {
    "path": "gist/client.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\"\"\"\nName:\n    gist\n\nUsage:\n    gist help\n    gist list\n    gist edit <id>\n    gist description <id> <desc>\n    gist info <id>\n    gist fork <id>\n    gist files <id>\n    gist delete <ids> ...\n    gist archive <id>\n    gist content <id> [<filename>] [--decrypt]\n    gist create <desc> [--public] [--encrypt] [FILES ...]\n    gist create <desc> [--public] [--encrypt] [--filename <filename>]\n    gist clone <id> [<name>]\n    gist version\n\nDescription:\n    This program provides a command line interface for interacting with github\n    gists.\n\nCommands:\n    help\n        Shows this documentation.\n\n    create\n        Create a new gist. A gist can be created in several ways. The content\n        of the gist can be piped to the gist,\n\n            $ echo \"this is the content\" | gist create \"gist description\"\n\n        The gist can be created from an existing set of files,\n\n            $ gist create \"gist description\" foo.txt bar.txt\n\n        The gist can be created on the fly,\n\n            $ gist create \"gist description\"\n\n        which will open the users default editor.\n\n        If you are creating a gist with a single file using either the pipe or\n        'on the fly' method above, you can also supply an optional argument to\n        name the file instead of using the default ('file1.txt'),\n\n            $ gist create \"gist description\" --filename foo.md\n\n        Note that the use of --filename is incompatible with passing in a list\n        of existing files.\n\n    edit\n        You can edit your gists directly with the 'edit' command. This command\n        will clone the gist to a temporary directory and open up the default\n        editor (defined by the EDITOR environment variable) to edit the files\n        in the gist. When the editor is exited the user is prompted to commit\n        the changes, which are then pushed back to the remote.\n\n    fork\n        Creates a fork of the specified gist.\n\n    description\n        Updates the description of a gist.\n\n    list\n        Returns a list of your gists. The gists are returned as,\n\n            2b1823252e8433ef8682 - mathematical divagations\n            a485ee9ddf6828d697be - notes on defenestration\n            589071c7a02b1823252e + abecedarian pericombobulations\n\n        The first column is the gists unique identifier; The second column\n        indicates whether the gist is public ('+') or private ('-'); The third\n        column is the description in the gist, which may be empty.\n\n    clone\n        Clones a gist to the current directory. This command will clone any\n        gist based on its unique identifier (i.e. not just the users) to the\n        current directory.\n\n    delete\n        Deletes the specified gist.\n\n    files\n        Returns a list of the files in the specified gist.\n\n    archive\n        Downloads the specified gist to a temporary directory and adds it to a\n        tarball, which is then moved to the current directory.\n\n    content\n        Writes the content of each file in the specified gist to the terminal,\n        e.g.\n\n            $ gist content c971fca7997aed65ddc9\n            foo.txt:\n            this is foo\n\n\n            bar.txt:\n            this is bar\n\n\n        For each file in the gist the first line is the name of the file\n        followed by a colon, and then the content of that file is written to\n        the terminal.\n\n        If a filename is given, only the content of the specified filename\n        will be printed.\n\n           $ gist content de42344a4ecb6250d6cea00d9da6d83a file1\n           content of file 1\n\n\n    info\n        This command provides a complete dump of the information about the gist\n        as a JSON object. It is mostly useful for debugging.\n\n    version\n        Returns the current version of gist.\n\n\"\"\"\n\nimport argparse\nimport codecs\nimport collections\nimport configparser\nimport json\nimport locale\nimport logging\nimport os\nimport pathlib\nimport platform\nimport shlex\nimport struct\nimport subprocess\nimport sys\nimport tempfile\n\nimport gnupg\n\nfrom . import gist\nfrom . import version\n\nif platform.system() != \"Windows\":\n    # those modules exist everywhere but on Windows\n    import termios\n    import fcntl\n\n\nlogger = logging.getLogger(\"gist\")\n\n\ndef wrap_stdout_for_unicode():\n    \"\"\"\n    We need to wrap stdout in order to properly handle piping unicode output.\n    However, detaching stdout can cause problems when trying to run tests.\n    Therefore this logic is placed inside this function so that it can be\n    disabled (monkeypatched) when tests are run.\n    \"\"\"\n\n    encoding = locale.getpreferredencoding()\n    sys.stdout = codecs.getwriter(encoding)(sys.stdout.detach())\n\n\nclass GistError(Exception):\n    def __init__(self, msg):\n        super(GistError, self).__init__(msg)\n        self.msg = msg\n\n\nclass GistMissingTokenError(GistError):\n    pass\n\n\nclass GistEmptyTokenError(GistError):\n    pass\n\n\nclass UserError(Exception):\n    pass\n\n\nclass FileInfo(collections.namedtuple(\"FileInfo\", \"name content\")):\n    pass\n\n\ndef terminal_width():\n    \"\"\"Returns the terminal width\n\n    Tries to determine the width of the terminal. If there is no terminal, then\n    None is returned instead.\n\n    \"\"\"\n    try:\n        if platform.system() == \"Windows\":\n            from ctypes import windll, create_string_buffer\n\n            # Reference: https://docs.microsoft.com/en-us/windows/console/getstdhandle # noqa\n            hStdErr = -12\n            get_console_info_fmtstr = \"hhhhHhhhhhh\"\n            herr = windll.kernel32.GetStdHandle(hStdErr)\n            csbi = create_string_buffer(struct.calcsize(get_console_info_fmtstr))\n            if not windll.kernel32.GetConsoleScreenBufferInfo(herr, csbi):\n                raise OSError(\"Failed to determine the terminal size\")\n            (_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack(\n                get_console_info_fmtstr, csbi.raw\n            )\n            tty_columns = right - left + 1\n            return tty_columns\n        else:\n            exitcode = fcntl.ioctl(\n                0, termios.TIOCGWINSZ, struct.pack(\"HHHH\", 0, 0, 0, 0)\n            )\n            h, w, hp, wp = struct.unpack(\"HHHH\", exitcode)\n        return w\n    except Exception:\n        pass\n\n\ndef elide(txt):\n    \"\"\"Elide the text to the width of the current terminal.\n\n    Arguments:\n        txt: the string to potentially elide\n\n    Returns:\n        A string that is no longer than the specified width.\n\n    \"\"\"\n    width = terminal_width()\n    if width is not None and width > 3:\n        try:\n            if len(txt) > width:\n                return txt[: width - 3] + \"...\"\n        except Exception:\n            pass\n\n    return txt\n\n\ndef get_value_from_command(value):\n    \"\"\"Return the value of a config option, potentially by running a command\n\n    When a config option begins with a ``!`` interpret the remaining text as a\n    shell command which when run prints the config option value to stdout.\n    Otherwise return the original string.\n\n    Argument:\n        value: value of an option returned from the config file.\n\n    \"\"\"\n    command = value.strip()\n    if command[0] == \"!\":\n        process = subprocess.Popen(\n            shlex.split(command[1:]), stdout=subprocess.PIPE, stderr=subprocess.PIPE\n        )\n        out, err = process.communicate()\n        if process.returncode != 0:\n            raise GistError(err)\n        return out.decode().strip()\n    return value\n\n\ndef get_personal_access_token(config):\n    \"\"\"Returns the users personal access token\n\n    Argument:\n        config: a configuration object\n\n    \"\"\"\n    try:\n        value = config.get(\"gist\", \"token\").strip()\n        if not value:\n            raise GistEmptyTokenError(\"An empty token is not valid\")\n\n    except configparser.NoOptionError:\n        raise GistMissingTokenError(\"Missing 'token' field in configuration\")\n\n    return get_value_from_command(value)\n\n\ndef alternative_editor(default):\n    \"\"\"Return the path to the 'alternatives' editor\n\n    Argument:\n        default: the default to use if the alternatives editor cannot be found.\n\n    \"\"\"\n    if os.path.exists(\"/usr/bin/editor\"):\n        return \"/usr/bin/editor\"\n\n    return default\n\n\ndef environment_editor(default):\n    \"\"\"Return the user specified environment default\n\n    Argument:\n        default: the default to use if the environment variable contains\n                nothing useful.\n\n    \"\"\"\n    editor = os.environ.get(\"EDITOR\", \"\").strip()\n    if editor != \"\":\n        return editor\n\n    return default\n\n\ndef configuration_editor(config, default):\n    \"\"\"Return the editor in the config file\n\n    Argument:\n        default: the default to use if there is no editor in the config\n\n    \"\"\"\n    try:\n        return config.get(\"gist\", \"editor\")\n    except configparser.NoOptionError:\n        return default\n\n\ndef homedir_config(default):\n    \"\"\"Return the path to the config file in the users home directory\n\n    Argument:\n        default: the default to use if ~/.gist does not exist.\n\n    \"\"\"\n    config_path = pathlib.Path(\"~\").expanduser() / \".gist\"\n    return config_path if config_path.is_file() else default\n\n\ndef alternative_config(default):\n    \"\"\"Return the path to the config file in .config directory\n\n    Argument:\n        default: the default to use if ~/.config/gist does not exist.\n\n    \"\"\"\n    config_path = pathlib.Path(\"~/.config/gist\").expanduser()\n    return config_path if config_path.is_file() else default\n\n\ndef xdg_data_config(default):\n    \"\"\"Return the path to the config file in XDG user config directory\n\n    Argument:\n        default: the default to use if either the XDG_DATA_HOME environment is\n            not set, or the XDG_DATA_HOME directory does not contain a 'gist'\n            file.\n\n    \"\"\"\n    config_path = os.environ.get(\"XDG_DATA_HOME\", None)\n    if config_path is not None:\n        config_path = pathlib.Path(config_path) / \"gist\"\n        if config_path.is_file():\n            return config_path\n\n    return default\n\n\ndef environment_config(default):\n    \"\"\"Return the path to the config file defined in an environment variable\n\n    Argument:\n        default: the default to use if the environment variable GIST_CONFIG has not been\n        set.\n\n    \"\"\"\n    config_path = os.environ.get(\"GIST_CONFIG\", None)\n    if config_path is not None:\n        config_path = pathlib.Path(config_path)\n        if config_path.is_file():\n            return config_path\n\n    return default\n\n\ndef load_config_file():\n    \"\"\"\n    Returns a ConfigParser object with any gist related configuration data\n    \"\"\"\n\n    config = configparser.ConfigParser()\n\n    config_path = homedir_config(None)\n    config_path = alternative_config(config_path)\n    config_path = xdg_data_config(config_path)\n    config_path = environment_config(config_path)\n\n    if config_path is None:\n        raise UserError(\"unable to find config file\")\n\n    try:\n        with open(config_path) as fp:\n            config.read_file(fp)\n\n    except Exception as e:\n        raise UserError(\"Unable to load configuration file: {0}\".format(e))\n\n    # Make sure the config contains a gist section. If it does not, create one so\n    # that the following code can simply assume it exists.\n    if not config.has_section(\"gist\"):\n        config.add_section(\"gist\")\n\n    return config\n\n\ndef handle_gist_list(gapi, args, *vargs):\n    \"\"\"Handle 'gist list' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: list\")\n    gists = gapi.list()\n    for info in gists:\n        public = \"+\" if info.public else \"-\"\n        desc = \"\" if info.desc is None else info.desc\n        line = u\"{} {} {}\".format(info.id, public, desc)\n        try:\n            print(elide(line))\n        except UnicodeEncodeError:\n            logger.error(\"unable to write gist {}\".format(info.id))\n\n\ndef handle_gist_edit(gapi, args, *vargs):\n    \"\"\"Handle 'gist edit' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: edit\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    gapi.edit(args.id)\n\n\ndef handle_gist_description(gapi, args, *vargs):\n    \"\"\"Handle 'gist description' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: description\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    logger.debug(u\"action: - {}\".format(args.desc))\n    gapi.description(args.id, args.desc)\n\n\ndef handle_gist_info(gapi, args, *vargs):\n    \"\"\"Handle 'gist info' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: info\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    info = gapi.info(args.id)\n    print(json.dumps(info, indent=2))\n\n\ndef handle_gist_fork(gapi, args, *vargs):\n    \"\"\"Handle 'gist fork' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: fork\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    _ = gapi.fork(args.id)\n\n\ndef handle_gist_files(gapi, args, *vargs):\n    \"\"\"Handle 'gist files' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: files\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    for f in gapi.files(args.id):\n        print(f)\n\n\ndef handle_gist_delete(gapi, args, *vargs):\n    \"\"\"Handle 'gist delete' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: delete\")\n    for gist_id in args.ids:\n        logger.debug(u\"action: - {}\".format(gist_id))\n        gapi.delete(gist_id)\n\n\ndef handle_gist_archive(gapi, args, *vargs):\n    \"\"\"Handle 'gist archive' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: archive\")\n    logger.debug(u\"action: - {}\".format(args.id))\n    gapi.archive(args.id)\n\n\ndef handle_gist_content(gapi, args, config, *vargs):\n    \"\"\"Handle 'gist content' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n        config: configuration data\n\n    \"\"\"\n    logger.debug(u\"action: content\")\n    logger.debug(u\"action: - {}\".format(args.id))\n\n    content = gapi.content(args.id)\n    gist_file = content.get(args.filename)\n\n    if args.decrypt:\n        if not config.has_option(\"gist\", \"gnupg-homedir\"):\n            raise GistError(\"gnupg-homedir missing from config file\")\n\n        homedir = config.get(\"gist\", \"gnupg-homedir\")\n        logger.debug(u\"action: - {}\".format(homedir))\n\n        gpg = gnupg.GPG(gnupghome=homedir, use_agent=True)\n        if gist_file is not None:\n            print(gpg.decrypt(gist_file).data.decode(\"utf-8\"))\n        else:\n            for name, lines in content.items():\n                lines = gpg.decrypt(lines).data.decode(\"utf-8\")\n                print(u\"{} (decrypted):\\n{}\\n\".format(name, lines))\n\n    else:\n        if gist_file is not None:\n            print(gist_file)\n        else:\n            for name, lines in content.items():\n                print(u\"{}:\\n{}\\n\".format(name, lines))\n\n\ndef handle_gist_create(gapi, args, config, editor, *vargs):\n    \"\"\"Handle 'gist create' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n        config: configuration data\n        editor: editor command to use to create gist content\n\n    \"\"\"\n    logger.debug(\"action: create\")\n\n    # If encryption is selected, perform an initial check to make sure that\n    # it is possible before processing any data.\n    if args.encrypt:\n        if not config.has_option(\"gist\", \"gnupg-homedir\"):\n            raise GistError(\"gnupg-homedir missing from config file\")\n\n        if not config.has_option(\"gist\", \"gnupg-fingerprint\"):\n            raise GistError(\"gnupg-fingerprint missing from config file\")\n\n    # Retrieve the data to add to the gist\n    files = list()\n\n    if sys.stdin.isatty():\n        if args.files:\n            logger.debug(\"action: - reading from files\")\n            for path in args.files:\n                name = os.path.basename(path)\n                with open(path, \"rb\") as fp:\n                    files.append(FileInfo(name, fp.read().decode(\"utf-8\")))\n\n        else:\n            logger.debug(\"action: - reading from editor\")\n\n            filename = \"file1.txt\" if args.filename is None else args.filename\n\n            # Determine whether the temporary file should be deleted\n            if config.has_option(\"gist\", \"delete-tempfiles\"):\n                delete = config.getboolean(\"gist\", \"delete-tempfiles\")\n            else:\n                delete = True\n\n            with tempfile.NamedTemporaryFile(\"wb+\", delete=delete) as fp:\n                logger.debug(\"action: - created {}\".format(fp.name))\n                os.system(\"{} {}\".format(editor, fp.name))\n                fp.flush()\n                fp.seek(0)\n\n                files.append(FileInfo(filename, fp.read().decode(\"utf-8\")))\n\n            if delete:\n                logger.debug(\"action: - removed {}\".format(fp.name))\n\n    else:\n        logger.debug(\"action: - reading from stdin\")\n\n        filename = \"file1.txt\" if args.filename is None else args.filename\n        files.append(FileInfo(filename, sys.stdin.read()))\n\n    # Ensure that there are no empty files\n    for file in files:\n        if len(file.content) == 0:\n            raise GistError(\"'{}' is empty\".format(file.name))\n\n    # Encrypt the files or leave them unmodified\n    if args.encrypt:\n        logger.debug(\"action: - encrypting content\")\n\n        fingerprint = config.get(\"gist\", \"gnupg-fingerprint\")\n        gnupghome = config.get(\"gist\", \"gnupg-homedir\")\n\n        gpg = gnupg.GPG(gnupghome=gnupghome, use_agent=True)\n        data = {}\n        for file in files:\n            cypher = gpg.encrypt(file.content.encode(\"utf-8\"), fingerprint)\n            content = cypher.data.decode(\"utf-8\")\n\n            data[\"{}.asc\".format(file.name)] = {\"content\": content}\n    else:\n        data = {file.name: {\"content\": file.content} for file in files}\n\n    print(gapi.create(args.desc, data, args.public))\n\n\ndef handle_gist_clone(gapi, args, *vargs):\n    \"\"\"Handle 'gist clone' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: clone\")\n    logger.debug(u\"action: - {} as {}\".format(args.id, args.name))\n    gapi.clone(args.id, args.name)\n\n\ndef handle_gist_version(gapi, args, *vargs):\n    \"\"\"Handle 'gist version' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: version\")\n    print(\"v{}\".format(version.__version__))\n\n\ndef handle_gist_help(gapi, args, *vargs):\n    \"\"\"Handle 'gist help' command\n\n    Arguments:\n        gapi: a GistAPI object\n        args: parsed command line arguments\n\n    \"\"\"\n    logger.debug(u\"action: help\")\n    print(__doc__)\n\n\ndef create_gist_list_parser(subparser):\n    \"\"\"Create parser for 'gist list' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"list\")\n    parser.set_defaults(func=handle_gist_list)\n\n\ndef create_gist_edit_parser(subparser):\n    \"\"\"Create parser for 'gist edit' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"edit\")\n    parser.add_argument(\"id\")\n    parser.set_defaults(func=handle_gist_edit)\n\n\ndef create_gist_description_parser(subparser):\n    \"\"\"Create parser for 'gist description' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"description\")\n    parser.add_argument(\"id\")\n    parser.add_argument(\"desc\")\n    parser.set_defaults(func=handle_gist_description)\n\n\ndef create_gist_info_parser(subparser):\n    \"\"\"Create parser for 'gist info' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"info\")\n    parser.add_argument(\"id\")\n    parser.set_defaults(func=handle_gist_info)\n\n\ndef create_gist_fork_parser(subparser):\n    \"\"\"Create parser for 'gist fork' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"fork\")\n    parser.add_argument(\"id\")\n    parser.set_defaults(func=handle_gist_fork)\n\n\ndef create_gist_files_parser(subparser):\n    \"\"\"Create parser for 'gist files' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"files\")\n    parser.add_argument(\"id\")\n    parser.set_defaults(func=handle_gist_files)\n\n\ndef create_gist_delete_parser(subparser):\n    \"\"\"Create parser for 'gist delete' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"delete\")\n    parser.add_argument(\"ids\", nargs=\"+\")\n    parser.set_defaults(func=handle_gist_delete)\n\n\ndef create_gist_archive_parser(subparser):\n    \"\"\"Create parser for 'gist archive' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"archive\")\n    parser.add_argument(\"id\")\n    parser.set_defaults(func=handle_gist_archive)\n\n\ndef create_gist_content_parser(subparser):\n    \"\"\"Create parser for 'gist content' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"content\")\n    parser.add_argument(\"id\")\n    parser.add_argument(\"filename\", nargs=\"?\", default=None)\n    parser.add_argument(\"--decrypt\", action=\"store_true\")\n    parser.set_defaults(func=handle_gist_content)\n\n\ndef create_gist_create_parser(subparser):\n    \"\"\"Create parser for 'gist create' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"create\")\n    parser.add_argument(\"desc\")\n    parser.add_argument(\"--encrypt\", action=\"store_true\")\n    parser.add_argument(\"--public\", action=\"store_true\")\n    parser.add_argument(\"--filename\")\n    parser.add_argument(\"files\", nargs=\"*\")\n    parser.set_defaults(func=handle_gist_create)\n\n\ndef create_gist_clone_parser(subparser):\n    \"\"\"Create parser for 'gist clone' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"clone\")\n    parser.add_argument(\"id\")\n    parser.add_argument(\"name\", nargs=\"?\", default=None)\n    parser.set_defaults(func=handle_gist_clone)\n\n\ndef create_gist_version_parser(subparser):\n    \"\"\"Create parser for 'gist version' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"version\")\n    parser.set_defaults(func=handle_gist_version)\n\n\ndef create_gist_help_parser(subparser):\n    \"\"\"Create parser for 'gist help' command\n\n    Arguments:\n        subparser: subparser object from primary parser\n\n    \"\"\"\n    parser = subparser.add_parser(\"help\")\n    parser.set_defaults(func=handle_gist_help)\n\n\ndef create_gist_parser():\n    \"\"\"Create main parser for 'gist' commands\"\"\"\n\n    # Subclass the ArgumentParser so that we can override the 'error' function\n    class Parser(argparse.ArgumentParser):\n        def __init__(self, *args, **kwargs):\n            kwargs[\"add_help\"] = False\n            super().__init__(*args, **kwargs)\n\n        def error(self, message):\n            raise UserError(message)\n\n    parser = Parser()\n    subparser = parser.add_subparsers()\n\n    create_gist_list_parser(subparser)\n    create_gist_edit_parser(subparser)\n    create_gist_description_parser(subparser)\n    create_gist_info_parser(subparser)\n    create_gist_fork_parser(subparser)\n    create_gist_files_parser(subparser)\n    create_gist_delete_parser(subparser)\n    create_gist_archive_parser(subparser)\n    create_gist_content_parser(subparser)\n    create_gist_create_parser(subparser)\n    create_gist_clone_parser(subparser)\n    create_gist_version_parser(subparser)\n    create_gist_help_parser(subparser)\n\n    return parser\n\n\ndef main(argv=sys.argv[1:], config=None):\n    try:\n        wrap_stdout_for_unicode()\n\n        # Setup logging\n        fmt = \"%(created).3f %(levelname)s[%(name)s] %(message)s\"\n        logging.basicConfig(format=fmt)\n\n        # Read in the configuration file\n        if config is None:\n            config = load_config_file()\n\n        try:\n            log_level = config.get(\"gist\", \"log-level\").upper()\n            logging.getLogger(\"gist\").setLevel(log_level)\n        except Exception:\n            logging.getLogger(\"gist\").setLevel(logging.ERROR)\n\n        # Determine the editor to use\n        editor = None\n        editor = alternative_editor(editor)\n        editor = environment_editor(editor)\n        editor = configuration_editor(config, editor)\n\n        if editor is None:\n            raise UserError(\"Unable to find an editor.\")\n\n        token = get_personal_access_token(config)\n        gapi = gist.GistAPI(token=token, editor=editor)\n\n        # Parser command line arguments\n        parser = create_gist_parser()\n        args = parser.parse_args(argv)\n        args.func(gapi, args, config, editor)\n\n    except UserError as e:\n        sys.stderr.write(u\"ERROR: {}\\n\".format(str(e)))\n        sys.stderr.flush()\n        sys.exit(1)\n    except GistError as e:\n        sys.stderr.write(u\"GIST: {}\\n\".format(e.msg))\n        sys.stderr.flush()\n        sys.exit(1)\n    except Exception as e:\n        logger.error(str(e))\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "gist/gist.py",
    "content": "import base64\nimport collections\nimport contextlib\nimport json\nimport os\nimport re\nimport shutil\nimport tarfile\nimport tempfile\n\nimport requests\n\nrequests.packages.urllib3.disable_warnings()\n\n\n@contextlib.contextmanager\ndef pushd(path):\n    original = os.getcwd()\n    os.chdir(path)\n    yield\n    os.chdir(original)\n\n\nclass GistInfo(collections.namedtuple(\"GistInfo\", \"id public desc\")):\n    pass\n\n\nclass authenticate(object):\n    \"\"\"\n    The class is used as a decorator to handle token authentication with\n    github.\n    \"\"\"\n\n    def __init__(self, func, method=\"GET\"):\n        \"\"\"Create an authenticate object\n\n        Arguments:\n            func: a function to decorate\n            method: the method of the request to construct\n\n        \"\"\"\n        self.func = func\n        self.owner = None\n        self.instance = None\n        self.headers = {\n            \"Accept-Encoding\": \"identity, deflate, compress, gzip\",\n            \"User-Agent\": \"python-requests/1.2.0\",\n            \"Accept\": \"application/vnd.github.v3.base64\",\n        }\n        self.method = method\n\n    @classmethod\n    def get(cls, func):\n        \"\"\"Create an authenticate object with a GET method\n\n        Arguments:\n            func: a function to decorate\n\n        \"\"\"\n        return cls(func, method=\"GET\")\n\n    @classmethod\n    def post(cls, func):\n        \"\"\"Create an authenticate object with a POST method\n\n        Arguments:\n            func: a function to decorate\n\n        \"\"\"\n        return cls(func, method=\"POST\")\n\n    @classmethod\n    def patch(cls, func):\n        \"\"\"Create an authenticate object with a PATCH method\n\n        Arguments:\n            func: a function to decorate\n\n        \"\"\"\n        return cls(func, method=\"PATCH\")\n\n    @classmethod\n    def delete(cls, func):\n        \"\"\"Create an authenticate object with a DELETE method\n\n        Arguments:\n            func: a function to decorate\n\n        \"\"\"\n        return cls(func, method=\"DELETE\")\n\n    def __get__(self, instance, owner):\n        \"\"\"Returns the __call__ method\n\n        This method is part of the data descriptor interface. It returns the\n        __call__ method, which wraps the original function.\n\n        \"\"\"\n        self.instance = instance\n        self.owner = owner\n        return self.__call__\n\n    def __call__(self, *args, **kwargs):\n        \"\"\"Wraps the original function and provides an initial request.\n\n        The request object is created with the instance token as a query\n        parameter, and specifies the required headers.\n\n        \"\"\"\n        try:\n            url = \"https://api.github.com/gists\"\n            token = self.instance.token\n            self.headers[\"Authorization\"] = \"token {}\".format(token)\n            request = requests.Request(self.method, url, headers=self.headers)\n            return self.func(self.instance, request, *args, **kwargs)\n        finally:\n            self.instance = None\n            self.owner = None\n\n\nclass GistAPI(object):\n    \"\"\"\n    This class defines the interface to github.\n    \"\"\"\n\n    def __init__(self, token, editor=None):\n        \"\"\"Create a GistAPI object\n\n        Arguments:\n            token: an authentication token\n            editor: path to the editor to use when editing a gist\n\n        \"\"\"\n        self.token = token\n        self.editor = editor\n        self.session = requests.Session()\n\n    def send(self, request, stem=None):\n        \"\"\"Prepare and send a request\n\n        Arguments:\n            request: a Request object that is not yet prepared\n            stem: a path to append to the root URL\n\n        Returns:\n            The response to the request\n\n        \"\"\"\n        if stem is not None:\n            request.url = request.url + \"/\" + stem.lstrip(\"/\")\n\n        prepped = self.session.prepare_request(request)\n        settings = self.session.merge_environment_settings(\n            url=prepped.url, proxies={}, stream=None, verify=None, cert=None\n        )\n\n        response = self.session.send(prepped, **settings)\n\n        if not response.ok:\n            response.raise_for_status()\n\n        return response\n\n    def list(self):\n        \"\"\"Returns a list of the users gists as GistInfo objects\n\n        Returns:\n            a list of GistInfo objects\n\n        \"\"\"\n        # Define the basic request. The per_page parameter is set to 100, which\n        # is the maximum github allows. If the user has more than one page of\n        # gists, this request object will be modified to retrieve each\n        # successive page of gists.\n        request = requests.Request(\n            \"GET\",\n            \"https://api.github.com/gists\",\n            headers={\n                \"Accept-Encoding\": \"identity, deflate, compress, gzip\",\n                \"User-Agent\": \"python-requests/1.2.0\",\n                \"Accept\": \"application/vnd.github.v3.base64\",\n                \"Authorization\": \"token {}\".format(self.token),\n            },\n            params={\"per_page\": 100},\n        )\n\n        # Github provides a 'link' header that contains information to\n        # navigate through a users page of gists. This regex is used to\n        # extract the URLs contained in this header, and to find the next page\n        # of gists.\n        pattern = re.compile(r'<([^>]*)>; rel=\"([^\"]*)\"')\n\n        gists = []\n        while True:\n\n            # Retrieve the next page of gists\n            try:\n                response = self.send(request).json()\n\n            except Exception:\n                break\n\n            # Extract the list of gists\n            for gist in response:\n                try:\n                    gists.append(\n                        GistInfo(\n                            gist[\"id\"],\n                            gist[\"public\"],\n                            gist[\"description\"],\n                        )\n                    )\n\n                except KeyError:\n                    continue\n\n            try:\n                link = response.headers[\"link\"]\n\n                # Search for the next page of gist. If a 'next' page is found,\n                # the URL is set to this new page and the iteration continues.\n                # If there is no next page, return the list of gists.\n                for result in pattern.finditer(link):\n                    url = result.group(1)\n                    rel = result.group(2)\n                    if rel == \"next\":\n                        request.url = url\n                        break\n                else:\n                    return gists\n\n            except Exception:\n                break\n\n        return gists\n\n    @authenticate.post\n    def create(self, request, desc, files, public=False):\n        \"\"\"Creates a gist\n\n        Arguments:\n            request: an initial request object\n            desc:    the gist description\n            files:   a list of files to add to the gist\n            public:  a flag to indicate whether the gist is public or not\n\n        Returns:\n            The URL to the newly created gist.\n\n        \"\"\"\n        request.data = json.dumps(\n            {\"description\": desc, \"public\": public, \"files\": files}\n        )\n        return self.send(request).json()[\"html_url\"]\n\n    @authenticate.delete\n    def delete(self, request, id):\n        \"\"\"Deletes a gist\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        \"\"\"\n        self.send(request, id)\n\n    @authenticate.get\n    def info(self, request, id):\n        \"\"\"Returns info about a given gist\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        Returns:\n            A dict containing the gist info\n\n        \"\"\"\n        return self.send(request, id).json()\n\n    @authenticate.get\n    def files(self, request, id):\n        \"\"\"Returns a list of files in the gist\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        Returns:\n            A list of the files\n\n        \"\"\"\n        gist = self.send(request, id).json()\n        return gist[\"files\"]\n\n    @authenticate.get\n    def content(self, request, id):\n        \"\"\"Returns the content of the gist\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        Returns:\n            A dict containing the contents of each file in the gist\n\n        \"\"\"\n        gist = self.send(request, id).json()\n\n        def convert(data):\n            return base64.b64decode(data).decode(\"utf-8\")\n\n        content = {}\n        for name, data in gist[\"files\"].items():\n            content[name] = convert(data[\"content\"])\n\n        return content\n\n    @authenticate.get\n    def archive(self, request, id):\n        \"\"\"Create an archive of a gist\n\n        The files in the gist are downloaded and added to a compressed archive\n        (tarball). If the ID of the gist was c78d925546e964b4b1df, the\n        resulting archive would be,\n\n            c78d925546e964b4b1df.tar.gz\n\n        The archive is created in the directory where the command is invoked.\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        \"\"\"\n        gist = self.send(request, id).json()\n\n        with tarfile.open(\"{}.tar.gz\".format(id), mode=\"w:gz\") as archive:\n            for name, data in gist[\"files\"].items():\n                with tempfile.NamedTemporaryFile(\"w+\") as fp:\n                    fp.write(data[\"content\"])\n                    fp.flush()\n                    archive.add(fp.name, arcname=name)\n\n    @authenticate.get\n    def edit(self, request, id):\n        \"\"\"Edit a gist\n\n        The files in the gist a cloned to a temporary directory and passed to\n        the default editor (defined by the EDITOR environmental variable). When\n        the user exits the editor, they will be provided with a prompt to\n        commit the changes, which will then be pushed to the remote.\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        \"\"\"\n        with pushd(tempfile.gettempdir()):\n            try:\n                self.clone(id)\n                with pushd(id):\n                    files = [f for f in os.listdir(\".\") if os.path.isfile(f)]\n                    quoted = ['\"{}\"'.format(f) for f in files]\n                    os.system(\"{} {}\".format(self.editor, \" \".join(quoted)))\n                    os.system(\"git commit -av && git push\")\n\n            finally:\n                shutil.rmtree(id)\n\n    @authenticate.post\n    def fork(self, request, id):\n        \"\"\"Fork a gist\n\n        Forks an existing gist.\n\n        Arguments:\n            request: an initial request object\n            id:      the gist identifier\n\n        \"\"\"\n        return self.send(request, \"{}/forks\".format(id))\n\n    @authenticate.patch\n    def description(self, request, id, description):\n        \"\"\"Updates the description of a gist\n\n        Arguments:\n            request:     an initial request object\n            id:          the id of the gist we want to edit the description for\n            description: the new description\n\n        \"\"\"\n        request.data = json.dumps({\"description\": description})\n        return self.send(request, id).json()[\"html_url\"]\n\n    def clone(self, id, name=None):\n        \"\"\"Clone a gist\n\n        Arguments:\n            id:   the gist identifier\n            name: the name to give the cloned repo\n\n        \"\"\"\n        url = \"git@gist.github.com:/{}\".format(id)\n\n        if name is None:\n            os.system(\"git clone {}\".format(url))\n        else:\n            os.system(\"git clone {} {}\".format(url, name))\n"
  },
  {
    "path": "gist/version.py",
    "content": "__version__ = \"0.10.6\"  # noqa\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"python-gist\"\nversion = \"0.10.6\"\ndescription = \"Manage github gists\"\nauthors = [\"Joshua Downer <joshua.downer@gmail.com>\"]\nlicense = \"MIT\"\nreadme = \"README.rst\"\nkeywords = [\"gist\", \"github\", \"git\"]\nhomepage = \"https://github.com/jdowner/gist\"\nrepository = \"https://github.com/jdowner/gist\"\ndocumentation = \"https://github.com/jdowner/gist\"\nclassifiers = [\n  \"Development Status :: 5 - Production/Stable\",\n  \"Environment :: Console\",\n  \"Intended Audience :: Developers\",\n  \"Intended Audience :: End Users/Desktop\",\n  \"Intended Audience :: System Administrators\",\n  \"License :: OSI Approved :: MIT License\",\n  \"Operating System :: Unix\",\n  \"Programming Language :: Python\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.6\",\n  \"Programming Language :: Python :: 3.7\",\n  \"Programming Language :: Python :: 3.8\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Topic :: Software Development\",\n  \"Topic :: Software Development :: Version Control\",\n  \"Topic :: Utilities\",\n]\npackages = [\n  {include = \"gist\"},\n]\n\n\n[tool.poetry.dependencies]\npython = \"^3.6\"\nrequests = \"^2.25.1\"\npython-gnupg = \"^0.4.7\"\n\n[tool.poetry.dev-dependencies]\npytest = \"^6.2.2\"\nresponses = \"^0.13.1\"\ntox = \"^3.23.0\"\ntox-poetry = \"^0.3.0\"\nflake8 = \"^3.9.0\"\nflake8-black = \"^0.2.1\"\nflake8-bugbear = \"^21.3.2\"\nflake8-import-order = \"^0.18.1\"\n\n[tool.poetry.scripts]\ngist = \"gist.client:main\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "appdirs==1.4.4; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\natomicwrites==1.4.0; python_version >= \"3.6\" and python_full_version < \"3.0.0\" and sys_platform == \"win32\" or sys_platform == \"win32\" and python_version >= \"3.6\" and python_full_version >= \"3.4.0\"\nattrs==21.2.0; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\nblack==20.8b1; python_version >= \"3.6\"\ncertifi==2021.5.30; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nchardet==4.0.0; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nclick==8.0.1; python_version >= \"3.6\"\ncolorama==0.4.4; python_version >= \"3.6\" and python_full_version < \"3.0.0\" and sys_platform == \"win32\" and platform_system == \"Windows\" or sys_platform == \"win32\" and python_version >= \"3.6\" and python_full_version >= \"3.5.0\" and platform_system == \"Windows\"\ndataclasses==0.8; python_version >= \"3.6\" and python_version < \"3.7\"\ndistlib==0.3.2; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nfilelock==3.0.12; python_version >= \"3\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3\"\nflake8-black==0.2.1\nflake8-bugbear==21.4.3; python_version >= \"3.6\"\nflake8-import-order==0.18.1\nflake8==3.9.2; (python_version >= \"2.7\" and python_full_version < \"3.0.0\") or (python_full_version >= \"3.5.0\")\nidna==2.10; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nimportlib-metadata==4.4.0; python_version < \"3.8\" and python_version >= \"3.6\" and (python_version >= \"3.6\" and python_full_version < \"3.0.0\" and python_version < \"3.8\" or python_full_version >= \"3.5.0\" and python_version < \"3.8\" and python_version >= \"3.6\") and (python_version >= \"3.6\" and python_full_version < \"3.0.0\" and python_version < \"3.8\" or python_full_version >= \"3.4.0\" and python_version >= \"3.6\" and python_version < \"3.8\")\nimportlib-resources==5.1.4; python_version >= \"3.6\" and python_full_version < \"3.0.0\" and python_version < \"3.7\" or python_full_version >= \"3.5.0\" and python_version < \"3.7\" and python_version >= \"3.6\"\niniconfig==1.1.1; python_version >= \"3.6\"\nmccabe==0.6.1; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nmypy-extensions==0.4.3; python_version >= \"3.6\"\npackaging==20.9; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\npathspec==0.8.1; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\npluggy==0.13.1; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\npy==1.10.0; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\npycodestyle==2.7.0; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\npyflakes==2.3.1; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\npyparsing==2.4.7; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.4.0\" and python_version >= \"3.6\"\npytest==6.2.4; python_version >= \"3.6\"\npython-gnupg==0.4.7\nregex==2021.4.4; python_version >= \"3.6\"\nrequests==2.25.1; (python_version >= \"2.7\" and python_full_version < \"3.0.0\") or (python_full_version >= \"3.5.0\")\nresponses==0.13.3; (python_version >= \"2.7\" and python_full_version < \"3.0.0\") or (python_full_version >= \"3.5.0\")\nsix==1.16.0; python_version >= \"3\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3\"\ntoml==0.10.2; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3.6\"\ntox-poetry==0.3.0\ntox==3.23.1; (python_version >= \"2.7\" and python_full_version < \"3.0.0\") or (python_full_version >= \"3.5.0\")\ntyped-ast==1.4.3; python_version >= \"3.6\"\ntyping-extensions==3.10.0.0; python_version < \"3.8\" and python_version >= \"3.6\"\nurllib3==1.26.5; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version < \"4\"\nvirtualenv==20.4.7; python_version >= \"3\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version >= \"3\"\nzipp==3.4.1; python_version < \"3.8\" and python_version >= \"3.6\" and (python_version >= \"3.6\" and python_full_version < \"3.0.0\" and python_version < \"3.7\" or python_full_version >= \"3.5.0\" and python_version < \"3.7\" and python_version >= \"3.6\")\n"
  },
  {
    "path": "requirements.txt",
    "content": "certifi==2021.5.30; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nchardet==4.0.0; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nidna==2.10; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\npython-gnupg==0.4.7\nrequests==2.25.1; (python_version >= \"2.7\" and python_full_version < \"3.0.0\") or (python_full_version >= \"3.5.0\")\nurllib3==1.26.5; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_version < \"4\"\n"
  },
  {
    "path": "share/gist-fzf.bash",
    "content": "#!/bin/bash\n\n# This function uses fzf to fuzzy match the output from 'gist list' in order to\n# enter a gist ID to commands that required IDs.\n__gist() {\n  local curr=${COMP_WORDS[COMP_CWORD]}\n  local cmd=${COMP_WORDS[1]}\n\n  COMPREPLY=()\n\n  case ${cmd} in\n    edit|description|archive|files|content|clone|info)\n      if (( ${COMP_CWORD} == 2 )); then\n        tput smcup\n        COMPREPLY=( $( gist list | fzf | cut -d\" \" -f1 ) )\n        tput rmcup\n      fi\n      ;;\n    delete)\n      tput smcup\n      COMPREPLY=( $( gist list | fzf | cut -d\" \" -f1 ) )\n      tput rmcup\n      ;;\n    create|list|fork)\n      ;;\n    *)\n      COMPREPLY=( $(compgen -W \"edit description delete create fork archive files content clone list info\" -- $curr) )\n      ;;\n  esac\n\n}\n\ncomplete -F __gist gist\n"
  },
  {
    "path": "share/gist-fzsl.bash",
    "content": "#!/bin/bash\n\n# This function uses fzsl to fuzzy match the output from 'gist list' in order to\n# enter a gist ID to commands that required IDs.\n__gist() {\n  local curr=${COMP_WORDS[COMP_CWORD]}\n  local cmd=${COMP_WORDS[1]}\n\n  COMPREPLY=()\n\n  case ${cmd} in\n    edit|description|archive|files|content|clone|info)\n      if (( ${COMP_CWORD} == 2 )); then\n        tput smcup\n        COMPREPLY=( $( gist list | fzsl | cut -d\" \" -f1 ) )\n        tput rmcup\n      fi\n      ;;\n    delete)\n      tput smcup\n      COMPREPLY=( $( gist list | fzsl | cut -d\" \" -f1 ) )\n      tput rmcup\n      ;;\n    create|list|fork)\n      ;;\n    *)\n      COMPREPLY=( $(compgen -W \"edit description delete create fork archive files content clone list info\" -- $curr) )\n      ;;\n  esac\n\n}\n\ncomplete -F __gist gist\n"
  },
  {
    "path": "share/gist.bash",
    "content": "#!/bin/bash\n\n__gist() {\n  local curr=${COMP_WORDS[COMP_CWORD]}\n  local cmd=${COMP_WORDS[1]}\n\n  COMPREPLY=()\n\n  case ${cmd} in\n    edit|description|delete|archive|files|content|clone|list|info|fork)\n      ;;\n    create)\n      if (( ${COMP_CWORD} >= 2 )); then\n        compopt -o filenames\n        COMPREPLY=( $(compgen -f -- ${curr}) )\n      fi\n      ;;\n    *)\n      COMPREPLY=( $(compgen -W \"edit description delete create fork archive files content clone list info\" -- $curr) )\n      ;;\n  esac\n\n}\n\ncomplete -F __gist gist\n"
  },
  {
    "path": "share/gist.fish",
    "content": "complete -c gist -f -a \"create\"      -d \"Creates a new gist\"\ncomplete -c gist -f -a \"edit\"        -d \"Edit the files in your gist\"\ncomplete -c gist -f -a \"list\"        -d \"Prints a list of your gists\"\ncomplete -c gist -f -a \"clone\"       -d \"Clones a gist\"\ncomplete -c gist -f -a \"delete\"      -d \"Deletes a gist from GitHub\"\ncomplete -c gist -f -a \"files\"       -d \"Prints a list of the files in a gist\"\ncomplete -c gist -f -a \"archive\"     -d \"Downloads a gist and creates a tarball\"\ncomplete -c gist -f -a \"content\"     -d \"Prints the content of the gist to stdout\"\ncomplete -c gist -f -a \"info\"        -d \"Prints detailed information about a gist\"\ncomplete -c gist -f -a \"version\"     -d \"Prints the current version\"\ncomplete -c gist -f -a \"description\" -d \"Updates the description of a gist.\""
  },
  {
    "path": "share/gist.zsh",
    "content": "#compdef gist\n\n_arguments \\\n  '*:: :->subcmds' && return 0\n\nlocal -a _first_arguments\n_first_arguments=(\n  'list:Print the list of your gists.'\n  'edit:Edit the files in your gist.'\n  'info:Print detailed information about the gist.'\n  'fork:Create a fork of the gist.'\n  'description:Update the description of the gist.'\n  'files:Print the list of files in a gist.'\n  'delete:Delete the gist from GitHub.'\n  'archive:Download the gist and create a tarball.'\n  'content:Print the contents of the gist to stdout.'\n  'create:Create a new gist.'\n  'clone:Clone the gist to a local repository.'\n)\n\nif (( CURRENT == 1 )); then\n  _describe -t commands \"gist subcommand\" _first_arguments\n  return\nfi\n\ncase \"$words[1]\" in\n  (create)\n    _arguments \\\n      '--public[Create a public gist.]' \\\n      '--encrypt[Encrypt the gist.]' \\\n      ':description: :' \\\n      '*:: :_files'\n  ;;\n  (content)\n    _arguments \\\n      '--decrypt[Decrypt the gist.]'\n  ;;\nesac\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/conftest.py",
    "content": "import configparser\nimport errno\nimport shlex\nimport subprocess\n\nimport gist.client\nimport gist.gist\nimport gnupg\nimport pytest\n\n\ndef kill_gpg_agent(homedir):\n    \"\"\"Try to kill the spawned gpg-agent\n\n    This is just a best-effort.  With gpg-1.x, the agent will most likely not\n    get started unless the user has done configuration to enforce it.  With\n    gpg-2.x, the agent will always be spawned as it is responsible for all\n    handling of private keys.  However, it was not until gpg-2.1.13 that\n    gpgconf accepted the homedir argument.\n\n    So:\n        - gpg-1.x probably has nothing to kill and the return value doesn't\n          matter\n        - <gpg-2.1.13 will leave an agent running after the tests exit\n        - >=gpg-2.1.13 will correctly kill the agent on shutdown.\n\n    This could be improved, but 2.1.13 was released in mid-2016 and a quick\n    survey of distros using gpg-2 shows they've all moved past that point.\n\n    \"\"\"\n    try:\n        subprocess.call(\n            shlex.split(\"gpgconf --homedir {} --kill gpg-agent\".format(homedir))\n        )\n    except OSError as e:\n        if e.errno != errno.ENOENT:\n            raise\n\n\n@pytest.fixture(autouse=True)\ndef disable_stdout_wrapper(monkeypatch):\n    monkeypatch.setattr(gist.client, \"wrap_stdout_for_unicode\", lambda: None)\n\n\n@pytest.fixture(autouse=True)\ndef editor(monkeypatch):\n    monkeypatch.setenv(\"EDITOR\", \"gist-placeholder\")\n\n\n@pytest.fixture\ndef gist_api():\n    return gist.gist.GistAPI(token=\"f00\")\n\n\n@pytest.fixture\ndef gnupghome():\n    return \"./tests/gnupg\"\n\n\n@pytest.fixture\ndef gpg(gnupghome):\n    try:\n        yield gnupg.GPG(gnupghome=gnupghome, use_agent=True)\n    finally:\n        kill_gpg_agent(gnupghome)\n\n\n@pytest.fixture\ndef fingerprint(gpg):\n    return gpg.list_keys()[0][\"fingerprint\"]\n\n\n@pytest.fixture\ndef encrypt(gpg, fingerprint):\n    def impl(text):\n        data = text.encode(\"utf-8\")\n        crypt = gpg.encrypt(data, fingerprint)\n        return crypt.data.decode(\"utf-8\")\n\n    return impl\n\n\n@pytest.fixture\ndef decrypt(gpg):\n    def impl(text):\n        \"\"\"Return the text as a decrypted string\"\"\"\n        data = text.encode(\"utf-8\")\n        crypt = gpg.decrypt(data)\n        return crypt.data.decode(\"utf-8\")\n\n    return impl\n\n\n@pytest.fixture\ndef config(gnupghome, fingerprint):\n    cfg = configparser.ConfigParser()\n    cfg.add_section(\"gist\")\n    cfg.set(\"gist\", \"token\", \"f00\")\n    cfg.set(\"gist\", \"gnupg-homedir\", gnupghome)\n    cfg.set(\"gist\", \"gnupg-fingerprint\", fingerprint)\n\n    return cfg\n\n\n@pytest.fixture\ndef gist_command(config, capsys):\n    def impl(cmd):\n        \"\"\"Return stdout produce by the specified CLI command\"\"\"\n        gist.client.main(argv=shlex.split(cmd), config=config)\n        return capsys.readouterr().out.splitlines()\n\n    return impl\n"
  },
  {
    "path": "tests/test_cli.py",
    "content": "import base64\nimport json\n\nimport responses\n\n\ndef b64encode(s):\n    \"\"\"Return the base64 encoding of a string\n\n    To support string encodings other than ascii, the content of a gist needs\n    to be uploaded in base64. Because python2.x and python3.x handle string\n    differently, it is necessary to be explicit about passing a string into\n    b64encode as bytes. This function handles the encoding of the string into\n    bytes, and then decodes the resulting bytes into a UTF-8 string, which is\n    returned.\n\n    \"\"\"\n    return base64.b64encode(s.encode(\"utf-8\")).decode(\"utf-8\")\n\n\n@responses.activate\ndef test_list(editor, gist_command):\n    message = list()\n    expected_gists = list()\n    for id in range(300):\n        desc = \"test-{}\".format(id)\n        public = id % 2 == 0\n\n        message.append(\n            {\n                \"id\": id,\n                \"description\": desc,\n                \"public\": public,\n            }\n        )\n\n        expected_gists.append(\"{} {} test-{}\".format(id, \"+\" if public else \"-\", id))\n\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists\",\n        body=json.dumps(message),\n        status=200,\n    )\n\n    gists = gist_command(\"list\")\n\n    assert gists == expected_gists\n\n\n@responses.activate\ndef test_content(editor, gist_command):\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists/1\",\n        body=json.dumps(\n            {\n                \"files\": {\n                    \"file-A.txt\": {\n                        \"filename\": \"file-A.txt\",\n                        \"content\": b64encode(\"test-content-A\"),\n                    },\n                    \"file-B.txt\": {\n                        \"filename\": \"file-B.txt\",\n                        \"content\": b64encode(\"test-content-\\u212C\"),\n                    },\n                },\n                \"description\": \"test-gist\",\n                \"public\": True,\n                \"id\": 1,\n            }\n        ),\n        status=200,\n    )\n\n    lines = gist_command(\"content 1\")\n\n    assert \"file-A.txt:\" in lines\n    assert \"test-content-A\" in lines\n    assert \"file-B.txt:\" in lines\n    assert \"test-content-\\u212c\" in lines\n"
  },
  {
    "path": "tests/test_cli_parser.py",
    "content": "import contextlib\nimport os\nimport shlex\nimport unittest.mock\n\nimport gist.client\nimport pytest\n\n\n@pytest.fixture(autouse=True)\ndef suppress_stderr():\n    with open(os.devnull, \"w\") as fd:\n        with contextlib.redirect_stderr(fd):\n            yield\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"create 'desc'\",\n        \"create --public 'desc'\",\n        \"create --encrypt 'desc'\",\n        \"create --public --encrypt 'desc'\",\n        \"create --public --encrypt 'desc' --filename file1\",\n        \"create --public --encrypt 'desc' file1 file2 file3\",\n        \"create 'desc' --public\",\n        \"create 'desc' --encrypt\",\n        \"create 'desc' --public --encrypt\",\n    ],\n)\ndef test_cli_parser_gist_create_valid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_create)\n    monkeypatch.setattr(gist.client, \"handle_gist_create\", handler)\n\n    gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"create --public\",\n        \"create --encrypt\",\n        \"create 'desc' --encrypt file1\",\n        \"create --public --encrypt 'desc' --filename file1 file2\",\n    ],\n)\ndef test_cli_parser_gist_create_invalid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_create)\n    monkeypatch.setattr(gist.client, \"handle_gist_create\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(command), config=config)\n\n\ndef test_cli_parser_gist_list_valid(monkeypatch, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_list)\n    monkeypatch.setattr(gist.client, \"handle_gist_list\", handler)\n\n    gist.client.main(argv=shlex.split(\"list\"), config=config)\n\n\ndef test_cli_parser_gist_list_invalid(monkeypatch, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_list)\n    monkeypatch.setattr(gist.client, \"handle_gist_list\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(\"list --no-an-option\"), config=config)\n\n\n@pytest.mark.parametrize(\"cmd\", [\"edit\", \"fork\", \"info\", \"files\", \"archive\"])\ndef test_cli_parser_gist_generic_valid(monkeypatch, cmd, config):\n    handler_name = \"handle_gist_{}\".format(cmd)\n    handler_mock = unittest.mock.create_autospec(getattr(gist.client, handler_name))\n    monkeypatch.setattr(gist.client, handler_name, handler_mock)\n\n    gist.client.main(argv=shlex.split(\"{} arg1\".format(cmd)), config=config)\n\n\n@pytest.mark.parametrize(\"args\", [\"\", \"arg1 arg2\"])\n@pytest.mark.parametrize(\"cmd\", [\"edit\", \"fork\", \"info\", \"files\", \"archive\"])\ndef test_cli_parser_gist_generic_invalid(monkeypatch, cmd, args, config):\n    handler_name = \"handle_gist_{}\".format(cmd)\n    handler_mock = unittest.mock.create_autospec(handler_name)\n    monkeypatch.setattr(gist.client, handler_name, handler_mock)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(\"{} {}\".format(cmd, args)), config=config)\n\n\n@pytest.mark.parametrize(\"command\", [\"description id desc\", \"description id 'desc'\"])\ndef test_cli_parser_gist_description_valid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_description)\n    monkeypatch.setattr(gist.client, \"handle_gist_description\", handler)\n\n    gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"description id\",\n        \"description id foo bar\",\n    ],\n)\ndef test_cli_parser_gist_description_invalid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_description)\n    monkeypatch.setattr(gist.client, \"handle_gist_description\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"content id\",\n        \"content id --decrypt\",\n        \"content id file1 --decrypt\",\n        \"content --decrypt id\",\n        \"content --decrypt id file1\",\n    ],\n)\ndef test_cli_parser_gist_content_valid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_content)\n    monkeypatch.setattr(gist.client, \"handle_gist_content\", handler)\n\n    gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"content\",\n        \"content --decrypt\",\n        \"content id file1 file2 --decrypt\",\n    ],\n)\ndef test_cli_parser_gist_content_invalid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_content)\n    monkeypatch.setattr(gist.client, \"handle_gist_content\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\n    \"command\",\n    [\n        \"clone id\",\n        \"clone id name\",\n        \"clone id 'long name'\",\n    ],\n)\ndef test_cli_parser_gist_clone_valid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_clone)\n    monkeypatch.setattr(gist.client, \"handle_gist_clone\", handler)\n\n    gist.client.main(argv=shlex.split(command), config=config)\n\n\n@pytest.mark.parametrize(\"command\", [\"clone\", \"clone id name1 name2\"])\ndef test_cli_parser_gist_clone_invalid(monkeypatch, command, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_clone)\n    monkeypatch.setattr(gist.client, \"handle_gist_clone\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(command), config=config)\n\n\ndef test_cli_parser_gist_version_valid(monkeypatch, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_version)\n    monkeypatch.setattr(gist.client, \"handle_gist_version\", handler)\n\n    gist.client.main(argv=shlex.split(\"version\"), config=config)\n\n\ndef test_cli_parser_gist_version_invalid(monkeypatch, config):\n    handler = unittest.mock.create_autospec(gist.client.handle_gist_version)\n    monkeypatch.setattr(gist.client, \"handle_gist_version\", handler)\n\n    with pytest.raises(SystemExit):\n        gist.client.main(argv=shlex.split(\"version arg\"), config=config)\n"
  },
  {
    "path": "tests/test_config.py",
    "content": "import configparser\n\nimport gist\nimport pytest\n\n\n@pytest.fixture\ndef config():\n    cfg = configparser.ConfigParser()\n    cfg.add_section(\"gist\")\n    return cfg\n\n\ndef test_get_value_from_command():\n    \"\"\"\n    Ensure that values which start with ``!`` are treated as commands and\n    return the string printed to stdout by the command, otherwise ensure\n    that the value passed to the function is returned.\n    \"\"\"\n    assert \"magic token\" == gist.client.get_value_from_command('!echo \"\\nmagic token\"')\n    assert \"magic token\" == gist.client.get_value_from_command(' !echo \"magic token\\n\"')\n    assert \"magic token\" == gist.client.get_value_from_command(\"magic token\")\n\n\ndef test_get_personal_access_token_missing(config):\n    with pytest.raises(gist.client.GistMissingTokenError):\n        gist.client.get_personal_access_token(config)\n\n\n@pytest.mark.parametrize(\"token\", [\"\", \"   \"])\ndef test_get_personal_access_token_empty(config, token):\n    config.set(\"gist\", \"token\", token)\n    with pytest.raises(gist.client.GistEmptyTokenError):\n        gist.client.get_personal_access_token(config)\n\n\n@pytest.mark.parametrize(\"token\", [\"   123   \", \"123abcABC0987\"])\ndef test_get_personal_access_token_valid(config, token):\n    config.set(\"gist\", \"token\", token)\n    gist.client.get_personal_access_token(config)\n"
  },
  {
    "path": "tests/test_gist.py",
    "content": "import base64\nimport json\nimport re\nimport sys\n\nimport responses\n\n\ndef b64encode(s):\n    \"\"\"Return the base64 encoding of a string\n\n    To support string encodings other than ascii, the content of a gist needs\n    to be uploaded in base64. Because python2.x and python3.x handle string\n    differently, it is necessary to be explicit about passing a string into\n    b64encode as bytes. This function handles the encoding of the string into\n    bytes, and then decodes the resulting bytes into a UTF-8 string, which is\n    returned.\n\n    \"\"\"\n    return base64.b64encode(s.encode(\"utf-8\")).decode(\"utf-8\")\n\n\n@responses.activate\ndef test_list(gist_api):\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists\",\n        body=json.dumps(\n            [\n                {\n                    \"id\": 1,\n                    \"description\": \"test-desc-A\",\n                    \"public\": True,\n                },\n                {\n                    \"id\": 2,\n                    \"description\": \"test-desc-\\u212C\",\n                    \"public\": False,\n                },\n            ]\n        ),\n        status=200,\n    )\n\n    gists = gist_api.list()\n\n    gistA = gists[0]\n    gistB = gists[1]\n\n    assert gistA.id == 1\n    assert gistA.desc == \"test-desc-A\"\n    assert gistA.public\n\n    assert gistB.id == 2\n    assert gistB.desc == \"test-desc-\\u212C\"\n    assert not gistB.public\n\n\n@responses.activate\ndef test_list_empty(gist_api):\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists\",\n        body=\"\",\n        status=200,\n    )\n\n    gists = gist_api.list()\n\n    assert len(gists) == 0\n\n\n@responses.activate\ndef test_content(gist_api):\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists/1\",\n        body=json.dumps(\n            {\n                \"files\": {\n                    \"file-A.txt\": {\n                        \"filename\": \"file-A.txt\",\n                        \"content\": b64encode(\"test-content-A\"),\n                    },\n                    \"file-B.txt\": {\n                        \"filename\": \"file-B.txt\",\n                        \"content\": b64encode(\"test-content-\\u212C\"),\n                    },\n                },\n                \"description\": \"test-gist\",\n                \"public\": True,\n                \"id\": 1,\n            }\n        ),\n        status=200,\n    )\n\n    content = gist_api.content(\"1\")\n\n    assert len(content) == 2\n    assert \"file-A.txt\" in content\n    assert \"file-B.txt\" in content\n    assert content[\"file-A.txt\"] == \"test-content-A\"\n    assert content[\"file-B.txt\"] == \"test-content-\\u212C\"\n\n\n@responses.activate\ndef test_create(gist_api):\n    def request_handler(request):\n        data = json.loads(request.body)\n        assert len(data[\"files\"]) == 2\n        assert \"test-file-A\" in data[\"files\"]\n\n        content = {k: v[\"content\"] for k, v in data[\"files\"].items()}\n\n        assert content[\"test-file-A\"] == \"test-content-A\"\n        assert content[\"test-file-B\"] == \"test-content-\\u212C\"\n\n        status = 200\n        headers = {}\n        body = json.dumps({\"html_url\": \"https://gist.github.com/gists/1\"})\n        return status, headers, body\n\n    responses.add_callback(\n        responses.POST,\n        \"https://api.github.com/gists\",\n        callback=request_handler,\n        content_type=\"application/json\",\n    )\n\n    public = True\n    desc = \"test-desc\"\n    files = {\n        \"test-file-A\": {\"content\": \"test-content-A\"},\n        \"test-file-B\": {\"content\": \"test-content-\\u212C\"},\n    }\n\n    gist_api.create(desc, files, public)\n\n\n@responses.activate\ndef test_gnupg_create_from_file(monkeypatch, decrypt, gist_command, tmp_path):\n    \"\"\"\n    This test checks that the content from a gist created from a file is\n    properly encrypted.\n\n    \"\"\"\n\n    # This is a work-around for testing with github actions. For some reason, stdin is\n    # no a TTY when run from there when it is normally. Hopefull, I can find a bettter\n    # solution in the future.\n    monkeypatch.setattr(sys.stdin, \"isatty\", lambda: True)\n\n    def request_handler(request):\n        # Decrypt the content of the request and check that it matches the\n        # original content.\n        body = json.loads(request.body)\n        data = list(body[\"files\"].values())\n        text = decrypt(data[0][\"content\"])\n\n        assert u\"test-content-\\u212C\" in text\n\n        status = 200\n        headers = {}\n        body = json.dumps({\"html_url\": \"https://gist.github.com/gists/1\"})\n\n        return status, headers, body\n\n    responses.add_callback(\n        responses.POST,\n        \"https://api.github.com/gists\",\n        callback=request_handler,\n        content_type=\"application/json\",\n    )\n\n    # Create a temporary file and write a test message to it\n    filename = tmp_path / \"gist-test-file.txt\"\n    with open(filename, \"w\", encoding=\"utf-8\") as fp:\n        fp.write(u\"test-content-\\u212C\\n\")\n\n    # It is important to escape the path here to ensure the separators are not stripped\n    # on Windows.\n    cmd = r'create --encrypt \"test-desc\" {}'.format(re.escape(str(filename)))\n\n    gist_command(cmd)\n\n\n@responses.activate\ndef test_gnupg_content(encrypt, gist_command):\n    \"\"\"\n    When encrypted content is received, check to make sure that it can be\n    properly decrypted.\n\n    \"\"\"\n\n    def b64encrypt(content):\n        return b64encode(encrypt(content))\n\n    responses.add(\n        responses.GET,\n        \"https://api.github.com/gists/1\",\n        body=json.dumps(\n            {\n                \"files\": {\n                    \"file-A.txt\": {\n                        \"filename\": \"file-A.txt\",\n                        \"content\": b64encrypt(u\"test-content-A\"),\n                    },\n                    \"file-B.txt\": {\n                        \"filename\": \"file-B.txt\",\n                        \"content\": b64encrypt(u\"test-content-\\u212C\"),\n                    },\n                },\n                \"description\": \"test-gist\",\n                \"public\": True,\n                \"id\": 1,\n            }\n        ),\n        status=200,\n    )\n\n    lines = gist_command(\"content 1 --decrypt\")\n\n    assert u\"file-A.txt (decrypted):\" in lines\n    assert u\"test-content-A\" in lines\n    assert u\"file-B.txt (decrypted):\" in lines\n    assert u\"test-content-\\u212C\" in lines\n\n\ndef test_gnupg(encrypt, decrypt):\n    \"\"\"\n    Make sure that the basic mechanism put in place for testing the\n    encryption used in gist works as expected.\n\n    \"\"\"\n    text = u\"this is a message \\u212C\"\n    cypher = encrypt(text)\n    plain = decrypt(cypher)\n\n    assert text != cypher\n    assert text == plain\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = py3{6,7,8,9}.*\n\n[testenv]\nwhitelist_externals = make\ncommands =\n  make lint test\n"
  }
]