Full Code of jdowner/gist for AI

master b967010044f8 cached
25 files
78.6 KB
20.2k tokens
113 symbols
1 requests
Download .txt
Repository: jdowner/gist
Branch: master
Commit: b967010044f8
Files: 25
Total size: 78.6 KB

Directory structure:
gitextract_stv4va3s/

├── .flake8
├── .github/
│   └── workflows/
│       └── ci.yaml
├── .gitignore
├── LICENSE
├── Makefile
├── README.rst
├── gist/
│   ├── __init__.py
│   ├── client.py
│   ├── gist.py
│   └── version.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── share/
│   ├── gist-fzf.bash
│   ├── gist-fzsl.bash
│   ├── gist.bash
│   ├── gist.fish
│   └── gist.zsh
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_cli.py
│   ├── test_cli_parser.py
│   ├── test_config.py
│   └── test_gist.py
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .flake8
================================================
[flake8]
max-line-length = 88
import-order-style = appnexus


================================================
FILE: .github/workflows/ci.yaml
================================================
name: gist continuous integration

on: [pull_request, push]

jobs:
  build:
    strategy:
      max-parallel: 16
      matrix:
        python-version: [3.6, 3.7, 3.8, 3.9]
        os: [macos-latest, ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v1
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v1
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install poetry
        poetry install
    - name: Lint
      run: |
        make lint
    - name: Test
      run: |
        make test


================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
*.eggs

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/
installed-files.txt
bin/gistc
tests/gnupg/*

.vscode
.envrc
.python-version
.vimlocal
tags
.venv/
.poetry-install-run


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015 Joshua Downer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: Makefile
================================================
SHELL=/bin/bash

TEST_FILES:=$(wildcard tests/*.py)
SRC_FILES:=$(wildcard gist/*.py)

REQ_BINS := poetry
$(foreach bin,$(REQ_BINS),\
    $(if $(shell which $(bin) 2> /dev/null),,$(error Missing required package `$(bin)`)))

.PHONY: build test lint tox clean export

.poetry-install-run:
	@poetry install --remove-untracked
	@touch .poetry-install-run

build:
	@poetry build

test: .poetry-install-run
	@poetry run pytest --ff -x -v -s tests

lint: .poetry-install-run
	@poetry run black --quiet --check $(TEST_FILES) $(SRC_FILES)
	@poetry run flake8 $(TEST_FILES) $(SRC_FILES)

tox: .poetry-install-run
	@poetry run tox

clean:
	git clean -xdf

export:
	@poetry export --without-hashes -o requirements.txt
	@poetry export --dev --without-hashes -o requirements-dev.txt


================================================
FILE: README.rst
================================================
==================================================
GIST
==================================================

'gist' is a command line interface for working with github gists. It provides
several methods for inspecting a users gists, and the ability to easily create
them.

.. image:: https://github.com/jdowner/gist/workflows/gist%20continuous%20integration/badge.svg
    :target: https://github.com/jdowner/gist


Requirements
--------------------------------------------------
Python 3.6, 3.7, 3.8, or 3.9 is required.


Installation
--------------------------------------------------

The preferred way to install 'gist' is from pypi.org using pip (or pip3),

::

  $ pip install python-gist

Alternatively, you can clone the repository and install it manually,

::

  $ pip install .

The 'share' directory contains a set of shell scripts that provide tab
completion and fuzzy search for gist. There are 3 different scripts for
tab-completion in bash: gist.bash, gist-fzf.bash, and gist-fzsl.bash. The first
provides simple tab completion and can be enable by adding the following to
your .bashrc file,

::

  source /usr/local/share/gist/gist.bash

The other scripts, gist-fzf.bash and fist-fzsl.bash, provide fuzzy matching of
gists using an ncurses interface (NB: these scripts require
`fzf <https://github.com/junegunn/fzf>`_ and `fzsl <https://github.com/jsbronder/fzsl>`_,
respectively).

The gist.fish script provides tab completion for the fish shell, and should be
copied to ~/.config/fish/completions.

The gist.zsh script provides tab completion for the zsh shell, and should be
copied to ~/.zsh as _gist. If not already in your ~/.zshrc file, you should add

::

  fpath=(${HOME}/.zsh $fpath)

To check that 'gist' is operating correctly, you can run the unit tests with,

::

  $ make test

Note that running the unit tests requires `poetry <https://python-poetry.org/>`_
to be available on your PATH.


Getting started
--------------------------------------------------

'gist' requires a personal access token for authentication. To create a token,
go to https://github.com/settings/tokens. The token needs to then be added
to a 'gist' configuration file that should have the form,

::

  [gist]
  token: <enter token here>
  editor: <path to editor>

The editor field is optional. If the default editor is specified through some
other mechanism 'gist' will try to infer it. Otherwise, you can use the config
file to ensure that 'gist' uses the editor you want it to use.

If the token string begins with ``!`` the text following is interpreted as a
shell command which, when executed, prints the token to stdout. For example::

  [gist]
  token: !gpg --decrypt github-token.gpg

The configuration file must be in one of the following,

::

  ${XDG_DATA_HOME}/gist
  ${HOME}/.config/gist
  ${HOME}/.gist

If more than one of these files exist, this is also the order of preference,
i.e. a configuration that is found in the ``${XDG_DATA_HOME}`` directory will be
taken in preference to ``${HOME}/.config/gist``.

Also, 'gist' assumes that you have set up your github account to use SSH keys so
that you can access your repositories without needing to provide a password.
Here__ is a link on setting up SSH keys with github.

__ https://help.github.com/articles/connecting-to-github-with-ssh/


Usage
--------------------------------------------------

'gist' is intended to make it easy to manage and use github gists from the
command line. There are several commands available:

::

  gist create      - creates a new gist
  gist edit        - edit the files in your gist
  gist description - updates the description of your gist
  gist list        - prints a list of your gists
  gist clone       - clones a gist
  gist delete      - deletes a gist or list of gists from github
  gist files       - prints a list of the files in a gist
  gist archive     - downloads a gist and creates a tarball
  gist content     - prints the content of the gist to stdout
  gist info        - prints detailed information about a gist
  gist version     - prints the current version
  gist help        - prints the help documentation


**gist create**

Most of the 'gist' commands are pretty simple and limited in what they can do.
'gist create' is a little different and offers more flexibility in how the user
can create the gist.

If you have a set of existing files that you want to turn into a gist,

::

  $ gist create "divide et impera" foo.txt bar.txt

where the quoted string is the description of the gist. Or, you may find it
useful to create a gist from content on your clipboard (say, using xclip),

::

  $ xclip -o | gist create "ipsa scientia potestas est"

Another option is to pipe the input into 'gist create' and have it automatically
put the content on github,

::

  $ echo $(cat) | gist create "credo quia absurdum est"

Finally, you can just call,

::

  $ gist create "a posse ad esse"

which will launch your default editor (defined by the EDITOR environment
variable).

In addition to creating gists using the above methods, it is also possible to
encrypt a gist if you have gnupg installed. Any of the above methods can be used
to create encrypted gists by simply adding the --encrypt flag to invocation.
For example,

::

  $ gist create "arcana imperii" --encrypt

will open the editor allowing you to create the content of the gist, which is
then encrypted and added to github. See the Configuration section for
information on how to enable gnupg support.


**gist edit**

You can edit your gists directly with the 'edit' command. This command will
clone the gist to a temporary directory and open up the default editor (defined
by the EDITOR environment variable) to edit the files in the gist. When the
editor is exited the user is prompted to commit the changes, which are then
pushed back to the remote.

**gist description**

You can update the description of your gist with the 'description' command.
You need to supply the gist ID and the new description. For example -

::

  $ gist description e1f5e95a1705cbfde144 "This is a new description"


**gist list**

Returns a list of your gists. The gists are returned as,

::

  2b1823252e8433ef8682 - mathematical divagations
  a485ee9ddf6828d697be - notes on defenestration
  589071c7a02b1823252e + abecedarian pericombobulations

The first column is the gists unique identifier; The second column indicates
whether the gist is public ('+') or private ('-'); The third column is the
description in the gist, which may be empty.


**gist clone**

Clones a gist to the current directory. This command will clone any gist based
on its unique identifier (i.e. not just the users) to the current directory.


**gist delete**

Deletes the specified gists from github.


**gist files**

Returns a list of the files in the specified gist.


**gist archive**

Downloads the specified gist to a temporary directory and adds it to a tarball,
which is then moved to the current directory.


**gist content**

Writes the content of each file in the specified gist to the terminal, e.g.

::

  $ gist content c971fca7997aed65ddc9
  foo.txt:
  this is foo


  bar.txt:
  this is bar


For each file in the gist the first line is the name of the file followed by a
colon, and then the content of that file is written to the terminal.

If a filename is given, only the content of the specified filename will be
printed.

::

  $ gist content de42344a4ecb6250d6cea00d9da6d83a file1
  content of file 1


If the contents of the gist is encrypted, it can be viewed in its decrypted
form by adding the --decrypt flag, e.g.

::

  $ gist content --decrypt 8fe557fb3771aa74edfd
  foo.txt.asc (decrypted):
  this is a secret


See the Configuration section for information on how to enable gnupg support.


**gist info**

This command provides a complete dump of the information about the gist as a
JSON object. It is mostly useful for debugging.


**gist version**

Simply prints the current version.


**gist help**

Prints out the help documentation.


Configuration
--------------------------------------------------

There are several parameters that can be added to a configuration file to
determine the behavior of gist. The configuration file itself is expected to
be one of the following paths,

::

  ${HOME}/.gist
  ${HOME}/.config/gist
  ${XDG_DATA_HOME}/gist

The configuration file follows the .ini style. The following is an example,

::

  [gist]
  token: dde7b84d1e0edf7454ab354934b6ab36b01bf00f
  editor: /usr/bin/vim
  gnupg-homedir: /home/user/.gnupg
  gnupg-fingerprint: 179F9650D9FC1BFE391620B4B13A7829D8DE8623
  delete-tempfiles: False

The only essential field in the configuration file is the token. This is the
authentication token from github that grants gist permission to access your
gists. The editor is the editor to use if the EDITOR environment is not set or
you wish to use a different editor. 'gnupg-homedir' is the directory where your
gnupg data are stored, and 'gnupg-fingerprint' is the fingerprint of the key to
use to encrypt data in your gists. Both gnupg fields are required to support
encryption/decryption.

The 'delete-tempfiles' option is used when gists are created from an editor.
The editor writes its contents to a temporary file, which is deleted by
default. The default behavior can be overridden by using the 'delete-tempfiles'
flag.


Contributors
--------------------------------------------------

Thank you to the following people for contributing to 'gist'!

* Eren Inan Canpolat (https://github.com/canpolat)
* Kaan Genç (https://github.com/SeriousBug)
* Eric James Michael Ritz (https://github.com/ejmr)
* Karan Parikh (https://github.com/karanparikh)
* Konstantin Krastev (https://github.com/grizmin)
* Brandon Davidson (https://github.com/brandond)
* jq170727 (https://github.com/jq170727)
* jsbronder (https://github.com/jsbronder)
* hugsy (https://github.com/hugsy)
* Kenneth Benzie (https://github.com/kbenzie)
* rtfmoz2 (https://github.com/rtfmoz2)


================================================
FILE: gist/__init__.py
================================================


================================================
FILE: gist/client.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Name:
    gist

Usage:
    gist help
    gist list
    gist edit <id>
    gist description <id> <desc>
    gist info <id>
    gist fork <id>
    gist files <id>
    gist delete <ids> ...
    gist archive <id>
    gist content <id> [<filename>] [--decrypt]
    gist create <desc> [--public] [--encrypt] [FILES ...]
    gist create <desc> [--public] [--encrypt] [--filename <filename>]
    gist clone <id> [<name>]
    gist version

Description:
    This program provides a command line interface for interacting with github
    gists.

Commands:
    help
        Shows this documentation.

    create
        Create a new gist. A gist can be created in several ways. The content
        of the gist can be piped to the gist,

            $ echo "this is the content" | gist create "gist description"

        The gist can be created from an existing set of files,

            $ gist create "gist description" foo.txt bar.txt

        The gist can be created on the fly,

            $ gist create "gist description"

        which will open the users default editor.

        If you are creating a gist with a single file using either the pipe or
        'on the fly' method above, you can also supply an optional argument to
        name the file instead of using the default ('file1.txt'),

            $ gist create "gist description" --filename foo.md

        Note that the use of --filename is incompatible with passing in a list
        of existing files.

    edit
        You can edit your gists directly with the 'edit' command. This command
        will clone the gist to a temporary directory and open up the default
        editor (defined by the EDITOR environment variable) to edit the files
        in the gist. When the editor is exited the user is prompted to commit
        the changes, which are then pushed back to the remote.

    fork
        Creates a fork of the specified gist.

    description
        Updates the description of a gist.

    list
        Returns a list of your gists. The gists are returned as,

            2b1823252e8433ef8682 - mathematical divagations
            a485ee9ddf6828d697be - notes on defenestration
            589071c7a02b1823252e + abecedarian pericombobulations

        The first column is the gists unique identifier; The second column
        indicates whether the gist is public ('+') or private ('-'); The third
        column is the description in the gist, which may be empty.

    clone
        Clones a gist to the current directory. This command will clone any
        gist based on its unique identifier (i.e. not just the users) to the
        current directory.

    delete
        Deletes the specified gist.

    files
        Returns a list of the files in the specified gist.

    archive
        Downloads the specified gist to a temporary directory and adds it to a
        tarball, which is then moved to the current directory.

    content
        Writes the content of each file in the specified gist to the terminal,
        e.g.

            $ gist content c971fca7997aed65ddc9
            foo.txt:
            this is foo


            bar.txt:
            this is bar


        For each file in the gist the first line is the name of the file
        followed by a colon, and then the content of that file is written to
        the terminal.

        If a filename is given, only the content of the specified filename
        will be printed.

           $ gist content de42344a4ecb6250d6cea00d9da6d83a file1
           content of file 1


    info
        This command provides a complete dump of the information about the gist
        as a JSON object. It is mostly useful for debugging.

    version
        Returns the current version of gist.

"""

import argparse
import codecs
import collections
import configparser
import json
import locale
import logging
import os
import pathlib
import platform
import shlex
import struct
import subprocess
import sys
import tempfile

import gnupg

from . import gist
from . import version

if platform.system() != "Windows":
    # those modules exist everywhere but on Windows
    import termios
    import fcntl


logger = logging.getLogger("gist")


def wrap_stdout_for_unicode():
    """
    We need to wrap stdout in order to properly handle piping unicode output.
    However, detaching stdout can cause problems when trying to run tests.
    Therefore this logic is placed inside this function so that it can be
    disabled (monkeypatched) when tests are run.
    """

    encoding = locale.getpreferredencoding()
    sys.stdout = codecs.getwriter(encoding)(sys.stdout.detach())


class GistError(Exception):
    def __init__(self, msg):
        super(GistError, self).__init__(msg)
        self.msg = msg


class GistMissingTokenError(GistError):
    pass


class GistEmptyTokenError(GistError):
    pass


class UserError(Exception):
    pass


class FileInfo(collections.namedtuple("FileInfo", "name content")):
    pass


def terminal_width():
    """Returns the terminal width

    Tries to determine the width of the terminal. If there is no terminal, then
    None is returned instead.

    """
    try:
        if platform.system() == "Windows":
            from ctypes import windll, create_string_buffer

            # Reference: https://docs.microsoft.com/en-us/windows/console/getstdhandle # noqa
            hStdErr = -12
            get_console_info_fmtstr = "hhhhHhhhhhh"
            herr = windll.kernel32.GetStdHandle(hStdErr)
            csbi = create_string_buffer(struct.calcsize(get_console_info_fmtstr))
            if not windll.kernel32.GetConsoleScreenBufferInfo(herr, csbi):
                raise OSError("Failed to determine the terminal size")
            (_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack(
                get_console_info_fmtstr, csbi.raw
            )
            tty_columns = right - left + 1
            return tty_columns
        else:
            exitcode = fcntl.ioctl(
                0, termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0)
            )
            h, w, hp, wp = struct.unpack("HHHH", exitcode)
        return w
    except Exception:
        pass


def elide(txt):
    """Elide the text to the width of the current terminal.

    Arguments:
        txt: the string to potentially elide

    Returns:
        A string that is no longer than the specified width.

    """
    width = terminal_width()
    if width is not None and width > 3:
        try:
            if len(txt) > width:
                return txt[: width - 3] + "..."
        except Exception:
            pass

    return txt


def get_value_from_command(value):
    """Return the value of a config option, potentially by running a command

    When a config option begins with a ``!`` interpret the remaining text as a
    shell command which when run prints the config option value to stdout.
    Otherwise return the original string.

    Argument:
        value: value of an option returned from the config file.

    """
    command = value.strip()
    if command[0] == "!":
        process = subprocess.Popen(
            shlex.split(command[1:]), stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )
        out, err = process.communicate()
        if process.returncode != 0:
            raise GistError(err)
        return out.decode().strip()
    return value


def get_personal_access_token(config):
    """Returns the users personal access token

    Argument:
        config: a configuration object

    """
    try:
        value = config.get("gist", "token").strip()
        if not value:
            raise GistEmptyTokenError("An empty token is not valid")

    except configparser.NoOptionError:
        raise GistMissingTokenError("Missing 'token' field in configuration")

    return get_value_from_command(value)


def alternative_editor(default):
    """Return the path to the 'alternatives' editor

    Argument:
        default: the default to use if the alternatives editor cannot be found.

    """
    if os.path.exists("/usr/bin/editor"):
        return "/usr/bin/editor"

    return default


def environment_editor(default):
    """Return the user specified environment default

    Argument:
        default: the default to use if the environment variable contains
                nothing useful.

    """
    editor = os.environ.get("EDITOR", "").strip()
    if editor != "":
        return editor

    return default


def configuration_editor(config, default):
    """Return the editor in the config file

    Argument:
        default: the default to use if there is no editor in the config

    """
    try:
        return config.get("gist", "editor")
    except configparser.NoOptionError:
        return default


def homedir_config(default):
    """Return the path to the config file in the users home directory

    Argument:
        default: the default to use if ~/.gist does not exist.

    """
    config_path = pathlib.Path("~").expanduser() / ".gist"
    return config_path if config_path.is_file() else default


def alternative_config(default):
    """Return the path to the config file in .config directory

    Argument:
        default: the default to use if ~/.config/gist does not exist.

    """
    config_path = pathlib.Path("~/.config/gist").expanduser()
    return config_path if config_path.is_file() else default


def xdg_data_config(default):
    """Return the path to the config file in XDG user config directory

    Argument:
        default: the default to use if either the XDG_DATA_HOME environment is
            not set, or the XDG_DATA_HOME directory does not contain a 'gist'
            file.

    """
    config_path = os.environ.get("XDG_DATA_HOME", None)
    if config_path is not None:
        config_path = pathlib.Path(config_path) / "gist"
        if config_path.is_file():
            return config_path

    return default


def environment_config(default):
    """Return the path to the config file defined in an environment variable

    Argument:
        default: the default to use if the environment variable GIST_CONFIG has not been
        set.

    """
    config_path = os.environ.get("GIST_CONFIG", None)
    if config_path is not None:
        config_path = pathlib.Path(config_path)
        if config_path.is_file():
            return config_path

    return default


def load_config_file():
    """
    Returns a ConfigParser object with any gist related configuration data
    """

    config = configparser.ConfigParser()

    config_path = homedir_config(None)
    config_path = alternative_config(config_path)
    config_path = xdg_data_config(config_path)
    config_path = environment_config(config_path)

    if config_path is None:
        raise UserError("unable to find config file")

    try:
        with open(config_path) as fp:
            config.read_file(fp)

    except Exception as e:
        raise UserError("Unable to load configuration file: {0}".format(e))

    # Make sure the config contains a gist section. If it does not, create one so
    # that the following code can simply assume it exists.
    if not config.has_section("gist"):
        config.add_section("gist")

    return config


def handle_gist_list(gapi, args, *vargs):
    """Handle 'gist list' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: list")
    gists = gapi.list()
    for info in gists:
        public = "+" if info.public else "-"
        desc = "" if info.desc is None else info.desc
        line = u"{} {} {}".format(info.id, public, desc)
        try:
            print(elide(line))
        except UnicodeEncodeError:
            logger.error("unable to write gist {}".format(info.id))


def handle_gist_edit(gapi, args, *vargs):
    """Handle 'gist edit' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: edit")
    logger.debug(u"action: - {}".format(args.id))
    gapi.edit(args.id)


def handle_gist_description(gapi, args, *vargs):
    """Handle 'gist description' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: description")
    logger.debug(u"action: - {}".format(args.id))
    logger.debug(u"action: - {}".format(args.desc))
    gapi.description(args.id, args.desc)


def handle_gist_info(gapi, args, *vargs):
    """Handle 'gist info' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: info")
    logger.debug(u"action: - {}".format(args.id))
    info = gapi.info(args.id)
    print(json.dumps(info, indent=2))


def handle_gist_fork(gapi, args, *vargs):
    """Handle 'gist fork' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: fork")
    logger.debug(u"action: - {}".format(args.id))
    _ = gapi.fork(args.id)


def handle_gist_files(gapi, args, *vargs):
    """Handle 'gist files' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: files")
    logger.debug(u"action: - {}".format(args.id))
    for f in gapi.files(args.id):
        print(f)


def handle_gist_delete(gapi, args, *vargs):
    """Handle 'gist delete' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: delete")
    for gist_id in args.ids:
        logger.debug(u"action: - {}".format(gist_id))
        gapi.delete(gist_id)


def handle_gist_archive(gapi, args, *vargs):
    """Handle 'gist archive' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: archive")
    logger.debug(u"action: - {}".format(args.id))
    gapi.archive(args.id)


def handle_gist_content(gapi, args, config, *vargs):
    """Handle 'gist content' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments
        config: configuration data

    """
    logger.debug(u"action: content")
    logger.debug(u"action: - {}".format(args.id))

    content = gapi.content(args.id)
    gist_file = content.get(args.filename)

    if args.decrypt:
        if not config.has_option("gist", "gnupg-homedir"):
            raise GistError("gnupg-homedir missing from config file")

        homedir = config.get("gist", "gnupg-homedir")
        logger.debug(u"action: - {}".format(homedir))

        gpg = gnupg.GPG(gnupghome=homedir, use_agent=True)
        if gist_file is not None:
            print(gpg.decrypt(gist_file).data.decode("utf-8"))
        else:
            for name, lines in content.items():
                lines = gpg.decrypt(lines).data.decode("utf-8")
                print(u"{} (decrypted):\n{}\n".format(name, lines))

    else:
        if gist_file is not None:
            print(gist_file)
        else:
            for name, lines in content.items():
                print(u"{}:\n{}\n".format(name, lines))


def handle_gist_create(gapi, args, config, editor, *vargs):
    """Handle 'gist create' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments
        config: configuration data
        editor: editor command to use to create gist content

    """
    logger.debug("action: create")

    # If encryption is selected, perform an initial check to make sure that
    # it is possible before processing any data.
    if args.encrypt:
        if not config.has_option("gist", "gnupg-homedir"):
            raise GistError("gnupg-homedir missing from config file")

        if not config.has_option("gist", "gnupg-fingerprint"):
            raise GistError("gnupg-fingerprint missing from config file")

    # Retrieve the data to add to the gist
    files = list()

    if sys.stdin.isatty():
        if args.files:
            logger.debug("action: - reading from files")
            for path in args.files:
                name = os.path.basename(path)
                with open(path, "rb") as fp:
                    files.append(FileInfo(name, fp.read().decode("utf-8")))

        else:
            logger.debug("action: - reading from editor")

            filename = "file1.txt" if args.filename is None else args.filename

            # Determine whether the temporary file should be deleted
            if config.has_option("gist", "delete-tempfiles"):
                delete = config.getboolean("gist", "delete-tempfiles")
            else:
                delete = True

            with tempfile.NamedTemporaryFile("wb+", delete=delete) as fp:
                logger.debug("action: - created {}".format(fp.name))
                os.system("{} {}".format(editor, fp.name))
                fp.flush()
                fp.seek(0)

                files.append(FileInfo(filename, fp.read().decode("utf-8")))

            if delete:
                logger.debug("action: - removed {}".format(fp.name))

    else:
        logger.debug("action: - reading from stdin")

        filename = "file1.txt" if args.filename is None else args.filename
        files.append(FileInfo(filename, sys.stdin.read()))

    # Ensure that there are no empty files
    for file in files:
        if len(file.content) == 0:
            raise GistError("'{}' is empty".format(file.name))

    # Encrypt the files or leave them unmodified
    if args.encrypt:
        logger.debug("action: - encrypting content")

        fingerprint = config.get("gist", "gnupg-fingerprint")
        gnupghome = config.get("gist", "gnupg-homedir")

        gpg = gnupg.GPG(gnupghome=gnupghome, use_agent=True)
        data = {}
        for file in files:
            cypher = gpg.encrypt(file.content.encode("utf-8"), fingerprint)
            content = cypher.data.decode("utf-8")

            data["{}.asc".format(file.name)] = {"content": content}
    else:
        data = {file.name: {"content": file.content} for file in files}

    print(gapi.create(args.desc, data, args.public))


def handle_gist_clone(gapi, args, *vargs):
    """Handle 'gist clone' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: clone")
    logger.debug(u"action: - {} as {}".format(args.id, args.name))
    gapi.clone(args.id, args.name)


def handle_gist_version(gapi, args, *vargs):
    """Handle 'gist version' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: version")
    print("v{}".format(version.__version__))


def handle_gist_help(gapi, args, *vargs):
    """Handle 'gist help' command

    Arguments:
        gapi: a GistAPI object
        args: parsed command line arguments

    """
    logger.debug(u"action: help")
    print(__doc__)


def create_gist_list_parser(subparser):
    """Create parser for 'gist list' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("list")
    parser.set_defaults(func=handle_gist_list)


def create_gist_edit_parser(subparser):
    """Create parser for 'gist edit' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("edit")
    parser.add_argument("id")
    parser.set_defaults(func=handle_gist_edit)


def create_gist_description_parser(subparser):
    """Create parser for 'gist description' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("description")
    parser.add_argument("id")
    parser.add_argument("desc")
    parser.set_defaults(func=handle_gist_description)


def create_gist_info_parser(subparser):
    """Create parser for 'gist info' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("info")
    parser.add_argument("id")
    parser.set_defaults(func=handle_gist_info)


def create_gist_fork_parser(subparser):
    """Create parser for 'gist fork' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("fork")
    parser.add_argument("id")
    parser.set_defaults(func=handle_gist_fork)


def create_gist_files_parser(subparser):
    """Create parser for 'gist files' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("files")
    parser.add_argument("id")
    parser.set_defaults(func=handle_gist_files)


def create_gist_delete_parser(subparser):
    """Create parser for 'gist delete' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("delete")
    parser.add_argument("ids", nargs="+")
    parser.set_defaults(func=handle_gist_delete)


def create_gist_archive_parser(subparser):
    """Create parser for 'gist archive' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("archive")
    parser.add_argument("id")
    parser.set_defaults(func=handle_gist_archive)


def create_gist_content_parser(subparser):
    """Create parser for 'gist content' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("content")
    parser.add_argument("id")
    parser.add_argument("filename", nargs="?", default=None)
    parser.add_argument("--decrypt", action="store_true")
    parser.set_defaults(func=handle_gist_content)


def create_gist_create_parser(subparser):
    """Create parser for 'gist create' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("create")
    parser.add_argument("desc")
    parser.add_argument("--encrypt", action="store_true")
    parser.add_argument("--public", action="store_true")
    parser.add_argument("--filename")
    parser.add_argument("files", nargs="*")
    parser.set_defaults(func=handle_gist_create)


def create_gist_clone_parser(subparser):
    """Create parser for 'gist clone' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("clone")
    parser.add_argument("id")
    parser.add_argument("name", nargs="?", default=None)
    parser.set_defaults(func=handle_gist_clone)


def create_gist_version_parser(subparser):
    """Create parser for 'gist version' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("version")
    parser.set_defaults(func=handle_gist_version)


def create_gist_help_parser(subparser):
    """Create parser for 'gist help' command

    Arguments:
        subparser: subparser object from primary parser

    """
    parser = subparser.add_parser("help")
    parser.set_defaults(func=handle_gist_help)


def create_gist_parser():
    """Create main parser for 'gist' commands"""

    # Subclass the ArgumentParser so that we can override the 'error' function
    class Parser(argparse.ArgumentParser):
        def __init__(self, *args, **kwargs):
            kwargs["add_help"] = False
            super().__init__(*args, **kwargs)

        def error(self, message):
            raise UserError(message)

    parser = Parser()
    subparser = parser.add_subparsers()

    create_gist_list_parser(subparser)
    create_gist_edit_parser(subparser)
    create_gist_description_parser(subparser)
    create_gist_info_parser(subparser)
    create_gist_fork_parser(subparser)
    create_gist_files_parser(subparser)
    create_gist_delete_parser(subparser)
    create_gist_archive_parser(subparser)
    create_gist_content_parser(subparser)
    create_gist_create_parser(subparser)
    create_gist_clone_parser(subparser)
    create_gist_version_parser(subparser)
    create_gist_help_parser(subparser)

    return parser


def main(argv=sys.argv[1:], config=None):
    try:
        wrap_stdout_for_unicode()

        # Setup logging
        fmt = "%(created).3f %(levelname)s[%(name)s] %(message)s"
        logging.basicConfig(format=fmt)

        # Read in the configuration file
        if config is None:
            config = load_config_file()

        try:
            log_level = config.get("gist", "log-level").upper()
            logging.getLogger("gist").setLevel(log_level)
        except Exception:
            logging.getLogger("gist").setLevel(logging.ERROR)

        # Determine the editor to use
        editor = None
        editor = alternative_editor(editor)
        editor = environment_editor(editor)
        editor = configuration_editor(config, editor)

        if editor is None:
            raise UserError("Unable to find an editor.")

        token = get_personal_access_token(config)
        gapi = gist.GistAPI(token=token, editor=editor)

        # Parser command line arguments
        parser = create_gist_parser()
        args = parser.parse_args(argv)
        args.func(gapi, args, config, editor)

    except UserError as e:
        sys.stderr.write(u"ERROR: {}\n".format(str(e)))
        sys.stderr.flush()
        sys.exit(1)
    except GistError as e:
        sys.stderr.write(u"GIST: {}\n".format(e.msg))
        sys.stderr.flush()
        sys.exit(1)
    except Exception as e:
        logger.error(str(e))
        sys.exit(1)


if __name__ == "__main__":
    main()


================================================
FILE: gist/gist.py
================================================
import base64
import collections
import contextlib
import json
import os
import re
import shutil
import tarfile
import tempfile

import requests

requests.packages.urllib3.disable_warnings()


@contextlib.contextmanager
def pushd(path):
    original = os.getcwd()
    os.chdir(path)
    yield
    os.chdir(original)


class GistInfo(collections.namedtuple("GistInfo", "id public desc")):
    pass


class authenticate(object):
    """
    The class is used as a decorator to handle token authentication with
    github.
    """

    def __init__(self, func, method="GET"):
        """Create an authenticate object

        Arguments:
            func: a function to decorate
            method: the method of the request to construct

        """
        self.func = func
        self.owner = None
        self.instance = None
        self.headers = {
            "Accept-Encoding": "identity, deflate, compress, gzip",
            "User-Agent": "python-requests/1.2.0",
            "Accept": "application/vnd.github.v3.base64",
        }
        self.method = method

    @classmethod
    def get(cls, func):
        """Create an authenticate object with a GET method

        Arguments:
            func: a function to decorate

        """
        return cls(func, method="GET")

    @classmethod
    def post(cls, func):
        """Create an authenticate object with a POST method

        Arguments:
            func: a function to decorate

        """
        return cls(func, method="POST")

    @classmethod
    def patch(cls, func):
        """Create an authenticate object with a PATCH method

        Arguments:
            func: a function to decorate

        """
        return cls(func, method="PATCH")

    @classmethod
    def delete(cls, func):
        """Create an authenticate object with a DELETE method

        Arguments:
            func: a function to decorate

        """
        return cls(func, method="DELETE")

    def __get__(self, instance, owner):
        """Returns the __call__ method

        This method is part of the data descriptor interface. It returns the
        __call__ method, which wraps the original function.

        """
        self.instance = instance
        self.owner = owner
        return self.__call__

    def __call__(self, *args, **kwargs):
        """Wraps the original function and provides an initial request.

        The request object is created with the instance token as a query
        parameter, and specifies the required headers.

        """
        try:
            url = "https://api.github.com/gists"
            token = self.instance.token
            self.headers["Authorization"] = "token {}".format(token)
            request = requests.Request(self.method, url, headers=self.headers)
            return self.func(self.instance, request, *args, **kwargs)
        finally:
            self.instance = None
            self.owner = None


class GistAPI(object):
    """
    This class defines the interface to github.
    """

    def __init__(self, token, editor=None):
        """Create a GistAPI object

        Arguments:
            token: an authentication token
            editor: path to the editor to use when editing a gist

        """
        self.token = token
        self.editor = editor
        self.session = requests.Session()

    def send(self, request, stem=None):
        """Prepare and send a request

        Arguments:
            request: a Request object that is not yet prepared
            stem: a path to append to the root URL

        Returns:
            The response to the request

        """
        if stem is not None:
            request.url = request.url + "/" + stem.lstrip("/")

        prepped = self.session.prepare_request(request)
        settings = self.session.merge_environment_settings(
            url=prepped.url, proxies={}, stream=None, verify=None, cert=None
        )

        response = self.session.send(prepped, **settings)

        if not response.ok:
            response.raise_for_status()

        return response

    def list(self):
        """Returns a list of the users gists as GistInfo objects

        Returns:
            a list of GistInfo objects

        """
        # Define the basic request. The per_page parameter is set to 100, which
        # is the maximum github allows. If the user has more than one page of
        # gists, this request object will be modified to retrieve each
        # successive page of gists.
        request = requests.Request(
            "GET",
            "https://api.github.com/gists",
            headers={
                "Accept-Encoding": "identity, deflate, compress, gzip",
                "User-Agent": "python-requests/1.2.0",
                "Accept": "application/vnd.github.v3.base64",
                "Authorization": "token {}".format(self.token),
            },
            params={"per_page": 100},
        )

        # Github provides a 'link' header that contains information to
        # navigate through a users page of gists. This regex is used to
        # extract the URLs contained in this header, and to find the next page
        # of gists.
        pattern = re.compile(r'<([^>]*)>; rel="([^"]*)"')

        gists = []
        while True:

            # Retrieve the next page of gists
            try:
                response = self.send(request).json()

            except Exception:
                break

            # Extract the list of gists
            for gist in response:
                try:
                    gists.append(
                        GistInfo(
                            gist["id"],
                            gist["public"],
                            gist["description"],
                        )
                    )

                except KeyError:
                    continue

            try:
                link = response.headers["link"]

                # Search for the next page of gist. If a 'next' page is found,
                # the URL is set to this new page and the iteration continues.
                # If there is no next page, return the list of gists.
                for result in pattern.finditer(link):
                    url = result.group(1)
                    rel = result.group(2)
                    if rel == "next":
                        request.url = url
                        break
                else:
                    return gists

            except Exception:
                break

        return gists

    @authenticate.post
    def create(self, request, desc, files, public=False):
        """Creates a gist

        Arguments:
            request: an initial request object
            desc:    the gist description
            files:   a list of files to add to the gist
            public:  a flag to indicate whether the gist is public or not

        Returns:
            The URL to the newly created gist.

        """
        request.data = json.dumps(
            {"description": desc, "public": public, "files": files}
        )
        return self.send(request).json()["html_url"]

    @authenticate.delete
    def delete(self, request, id):
        """Deletes a gist

        Arguments:
            request: an initial request object
            id:      the gist identifier

        """
        self.send(request, id)

    @authenticate.get
    def info(self, request, id):
        """Returns info about a given gist

        Arguments:
            request: an initial request object
            id:      the gist identifier

        Returns:
            A dict containing the gist info

        """
        return self.send(request, id).json()

    @authenticate.get
    def files(self, request, id):
        """Returns a list of files in the gist

        Arguments:
            request: an initial request object
            id:      the gist identifier

        Returns:
            A list of the files

        """
        gist = self.send(request, id).json()
        return gist["files"]

    @authenticate.get
    def content(self, request, id):
        """Returns the content of the gist

        Arguments:
            request: an initial request object
            id:      the gist identifier

        Returns:
            A dict containing the contents of each file in the gist

        """
        gist = self.send(request, id).json()

        def convert(data):
            return base64.b64decode(data).decode("utf-8")

        content = {}
        for name, data in gist["files"].items():
            content[name] = convert(data["content"])

        return content

    @authenticate.get
    def archive(self, request, id):
        """Create an archive of a gist

        The files in the gist are downloaded and added to a compressed archive
        (tarball). If the ID of the gist was c78d925546e964b4b1df, the
        resulting archive would be,

            c78d925546e964b4b1df.tar.gz

        The archive is created in the directory where the command is invoked.

        Arguments:
            request: an initial request object
            id:      the gist identifier

        """
        gist = self.send(request, id).json()

        with tarfile.open("{}.tar.gz".format(id), mode="w:gz") as archive:
            for name, data in gist["files"].items():
                with tempfile.NamedTemporaryFile("w+") as fp:
                    fp.write(data["content"])
                    fp.flush()
                    archive.add(fp.name, arcname=name)

    @authenticate.get
    def edit(self, request, id):
        """Edit a gist

        The files in the gist a cloned to a temporary directory and passed to
        the default editor (defined by the EDITOR environmental variable). When
        the user exits the editor, they will be provided with a prompt to
        commit the changes, which will then be pushed to the remote.

        Arguments:
            request: an initial request object
            id:      the gist identifier

        """
        with pushd(tempfile.gettempdir()):
            try:
                self.clone(id)
                with pushd(id):
                    files = [f for f in os.listdir(".") if os.path.isfile(f)]
                    quoted = ['"{}"'.format(f) for f in files]
                    os.system("{} {}".format(self.editor, " ".join(quoted)))
                    os.system("git commit -av && git push")

            finally:
                shutil.rmtree(id)

    @authenticate.post
    def fork(self, request, id):
        """Fork a gist

        Forks an existing gist.

        Arguments:
            request: an initial request object
            id:      the gist identifier

        """
        return self.send(request, "{}/forks".format(id))

    @authenticate.patch
    def description(self, request, id, description):
        """Updates the description of a gist

        Arguments:
            request:     an initial request object
            id:          the id of the gist we want to edit the description for
            description: the new description

        """
        request.data = json.dumps({"description": description})
        return self.send(request, id).json()["html_url"]

    def clone(self, id, name=None):
        """Clone a gist

        Arguments:
            id:   the gist identifier
            name: the name to give the cloned repo

        """
        url = "git@gist.github.com:/{}".format(id)

        if name is None:
            os.system("git clone {}".format(url))
        else:
            os.system("git clone {} {}".format(url, name))


================================================
FILE: gist/version.py
================================================
__version__ = "0.10.6"  # noqa


================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "python-gist"
version = "0.10.6"
description = "Manage github gists"
authors = ["Joshua Downer <joshua.downer@gmail.com>"]
license = "MIT"
readme = "README.rst"
keywords = ["gist", "github", "git"]
homepage = "https://github.com/jdowner/gist"
repository = "https://github.com/jdowner/gist"
documentation = "https://github.com/jdowner/gist"
classifiers = [
  "Development Status :: 5 - Production/Stable",
  "Environment :: Console",
  "Intended Audience :: Developers",
  "Intended Audience :: End Users/Desktop",
  "Intended Audience :: System Administrators",
  "License :: OSI Approved :: MIT License",
  "Operating System :: Unix",
  "Programming Language :: Python",
  "Programming Language :: Python :: 3",
  "Programming Language :: Python :: 3.6",
  "Programming Language :: Python :: 3.7",
  "Programming Language :: Python :: 3.8",
  "Programming Language :: Python :: 3.9",
  "Topic :: Software Development",
  "Topic :: Software Development :: Version Control",
  "Topic :: Utilities",
]
packages = [
  {include = "gist"},
]


[tool.poetry.dependencies]
python = "^3.6"
requests = "^2.25.1"
python-gnupg = "^0.4.7"

[tool.poetry.dev-dependencies]
pytest = "^6.2.2"
responses = "^0.13.1"
tox = "^3.23.0"
tox-poetry = "^0.3.0"
flake8 = "^3.9.0"
flake8-black = "^0.2.1"
flake8-bugbear = "^21.3.2"
flake8-import-order = "^0.18.1"

[tool.poetry.scripts]
gist = "gist.client:main"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"



================================================
FILE: requirements-dev.txt
================================================
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"
atomicwrites==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"
attrs==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"
black==20.8b1; python_version >= "3.6"
certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
click==8.0.1; python_version >= "3.6"
colorama==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"
dataclasses==0.8; python_version >= "3.6" and python_version < "3.7"
distlib==0.3.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
filelock==3.0.12; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3"
flake8-black==0.2.1
flake8-bugbear==21.4.3; python_version >= "3.6"
flake8-import-order==0.18.1
flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
importlib-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")
importlib-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"
iniconfig==1.1.1; python_version >= "3.6"
mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
mypy-extensions==0.4.3; python_version >= "3.6"
packaging==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"
pathspec==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"
pluggy==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"
py==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"
pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
pyparsing==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"
pytest==6.2.4; python_version >= "3.6"
python-gnupg==0.4.7
regex==2021.4.4; python_version >= "3.6"
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
six==1.16.0; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3"
toml==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"
tox-poetry==0.3.0
tox==3.23.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
typed-ast==1.4.3; python_version >= "3.6"
typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6"
urllib3==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"
virtualenv==20.4.7; python_version >= "3" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3"
zipp==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")


================================================
FILE: requirements.txt
================================================
certifi==2021.5.30; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
chardet==4.0.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
idna==2.10; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0"
python-gnupg==0.4.7
requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
urllib3==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"


================================================
FILE: share/gist-fzf.bash
================================================
#!/bin/bash

# This function uses fzf to fuzzy match the output from 'gist list' in order to
# enter a gist ID to commands that required IDs.
__gist() {
  local curr=${COMP_WORDS[COMP_CWORD]}
  local cmd=${COMP_WORDS[1]}

  COMPREPLY=()

  case ${cmd} in
    edit|description|archive|files|content|clone|info)
      if (( ${COMP_CWORD} == 2 )); then
        tput smcup
        COMPREPLY=( $( gist list | fzf | cut -d" " -f1 ) )
        tput rmcup
      fi
      ;;
    delete)
      tput smcup
      COMPREPLY=( $( gist list | fzf | cut -d" " -f1 ) )
      tput rmcup
      ;;
    create|list|fork)
      ;;
    *)
      COMPREPLY=( $(compgen -W "edit description delete create fork archive files content clone list info" -- $curr) )
      ;;
  esac

}

complete -F __gist gist


================================================
FILE: share/gist-fzsl.bash
================================================
#!/bin/bash

# This function uses fzsl to fuzzy match the output from 'gist list' in order to
# enter a gist ID to commands that required IDs.
__gist() {
  local curr=${COMP_WORDS[COMP_CWORD]}
  local cmd=${COMP_WORDS[1]}

  COMPREPLY=()

  case ${cmd} in
    edit|description|archive|files|content|clone|info)
      if (( ${COMP_CWORD} == 2 )); then
        tput smcup
        COMPREPLY=( $( gist list | fzsl | cut -d" " -f1 ) )
        tput rmcup
      fi
      ;;
    delete)
      tput smcup
      COMPREPLY=( $( gist list | fzsl | cut -d" " -f1 ) )
      tput rmcup
      ;;
    create|list|fork)
      ;;
    *)
      COMPREPLY=( $(compgen -W "edit description delete create fork archive files content clone list info" -- $curr) )
      ;;
  esac

}

complete -F __gist gist


================================================
FILE: share/gist.bash
================================================
#!/bin/bash

__gist() {
  local curr=${COMP_WORDS[COMP_CWORD]}
  local cmd=${COMP_WORDS[1]}

  COMPREPLY=()

  case ${cmd} in
    edit|description|delete|archive|files|content|clone|list|info|fork)
      ;;
    create)
      if (( ${COMP_CWORD} >= 2 )); then
        compopt -o filenames
        COMPREPLY=( $(compgen -f -- ${curr}) )
      fi
      ;;
    *)
      COMPREPLY=( $(compgen -W "edit description delete create fork archive files content clone list info" -- $curr) )
      ;;
  esac

}

complete -F __gist gist


================================================
FILE: share/gist.fish
================================================
complete -c gist -f -a "create"      -d "Creates a new gist"
complete -c gist -f -a "edit"        -d "Edit the files in your gist"
complete -c gist -f -a "list"        -d "Prints a list of your gists"
complete -c gist -f -a "clone"       -d "Clones a gist"
complete -c gist -f -a "delete"      -d "Deletes a gist from GitHub"
complete -c gist -f -a "files"       -d "Prints a list of the files in a gist"
complete -c gist -f -a "archive"     -d "Downloads a gist and creates a tarball"
complete -c gist -f -a "content"     -d "Prints the content of the gist to stdout"
complete -c gist -f -a "info"        -d "Prints detailed information about a gist"
complete -c gist -f -a "version"     -d "Prints the current version"
complete -c gist -f -a "description" -d "Updates the description of a gist."

================================================
FILE: share/gist.zsh
================================================
#compdef gist

_arguments \
  '*:: :->subcmds' && return 0

local -a _first_arguments
_first_arguments=(
  'list:Print the list of your gists.'
  'edit:Edit the files in your gist.'
  'info:Print detailed information about the gist.'
  'fork:Create a fork of the gist.'
  'description:Update the description of the gist.'
  'files:Print the list of files in a gist.'
  'delete:Delete the gist from GitHub.'
  'archive:Download the gist and create a tarball.'
  'content:Print the contents of the gist to stdout.'
  'create:Create a new gist.'
  'clone:Clone the gist to a local repository.'
)

if (( CURRENT == 1 )); then
  _describe -t commands "gist subcommand" _first_arguments
  return
fi

case "$words[1]" in
  (create)
    _arguments \
      '--public[Create a public gist.]' \
      '--encrypt[Encrypt the gist.]' \
      ':description: :' \
      '*:: :_files'
  ;;
  (content)
    _arguments \
      '--decrypt[Decrypt the gist.]'
  ;;
esac


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/conftest.py
================================================
import configparser
import errno
import shlex
import subprocess

import gist.client
import gist.gist
import gnupg
import pytest


def kill_gpg_agent(homedir):
    """Try to kill the spawned gpg-agent

    This is just a best-effort.  With gpg-1.x, the agent will most likely not
    get started unless the user has done configuration to enforce it.  With
    gpg-2.x, the agent will always be spawned as it is responsible for all
    handling of private keys.  However, it was not until gpg-2.1.13 that
    gpgconf accepted the homedir argument.

    So:
        - gpg-1.x probably has nothing to kill and the return value doesn't
          matter
        - <gpg-2.1.13 will leave an agent running after the tests exit
        - >=gpg-2.1.13 will correctly kill the agent on shutdown.

    This could be improved, but 2.1.13 was released in mid-2016 and a quick
    survey of distros using gpg-2 shows they've all moved past that point.

    """
    try:
        subprocess.call(
            shlex.split("gpgconf --homedir {} --kill gpg-agent".format(homedir))
        )
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise


@pytest.fixture(autouse=True)
def disable_stdout_wrapper(monkeypatch):
    monkeypatch.setattr(gist.client, "wrap_stdout_for_unicode", lambda: None)


@pytest.fixture(autouse=True)
def editor(monkeypatch):
    monkeypatch.setenv("EDITOR", "gist-placeholder")


@pytest.fixture
def gist_api():
    return gist.gist.GistAPI(token="f00")


@pytest.fixture
def gnupghome():
    return "./tests/gnupg"


@pytest.fixture
def gpg(gnupghome):
    try:
        yield gnupg.GPG(gnupghome=gnupghome, use_agent=True)
    finally:
        kill_gpg_agent(gnupghome)


@pytest.fixture
def fingerprint(gpg):
    return gpg.list_keys()[0]["fingerprint"]


@pytest.fixture
def encrypt(gpg, fingerprint):
    def impl(text):
        data = text.encode("utf-8")
        crypt = gpg.encrypt(data, fingerprint)
        return crypt.data.decode("utf-8")

    return impl


@pytest.fixture
def decrypt(gpg):
    def impl(text):
        """Return the text as a decrypted string"""
        data = text.encode("utf-8")
        crypt = gpg.decrypt(data)
        return crypt.data.decode("utf-8")

    return impl


@pytest.fixture
def config(gnupghome, fingerprint):
    cfg = configparser.ConfigParser()
    cfg.add_section("gist")
    cfg.set("gist", "token", "f00")
    cfg.set("gist", "gnupg-homedir", gnupghome)
    cfg.set("gist", "gnupg-fingerprint", fingerprint)

    return cfg


@pytest.fixture
def gist_command(config, capsys):
    def impl(cmd):
        """Return stdout produce by the specified CLI command"""
        gist.client.main(argv=shlex.split(cmd), config=config)
        return capsys.readouterr().out.splitlines()

    return impl


================================================
FILE: tests/test_cli.py
================================================
import base64
import json

import responses


def b64encode(s):
    """Return the base64 encoding of a string

    To support string encodings other than ascii, the content of a gist needs
    to be uploaded in base64. Because python2.x and python3.x handle string
    differently, it is necessary to be explicit about passing a string into
    b64encode as bytes. This function handles the encoding of the string into
    bytes, and then decodes the resulting bytes into a UTF-8 string, which is
    returned.

    """
    return base64.b64encode(s.encode("utf-8")).decode("utf-8")


@responses.activate
def test_list(editor, gist_command):
    message = list()
    expected_gists = list()
    for id in range(300):
        desc = "test-{}".format(id)
        public = id % 2 == 0

        message.append(
            {
                "id": id,
                "description": desc,
                "public": public,
            }
        )

        expected_gists.append("{} {} test-{}".format(id, "+" if public else "-", id))

    responses.add(
        responses.GET,
        "https://api.github.com/gists",
        body=json.dumps(message),
        status=200,
    )

    gists = gist_command("list")

    assert gists == expected_gists


@responses.activate
def test_content(editor, gist_command):
    responses.add(
        responses.GET,
        "https://api.github.com/gists/1",
        body=json.dumps(
            {
                "files": {
                    "file-A.txt": {
                        "filename": "file-A.txt",
                        "content": b64encode("test-content-A"),
                    },
                    "file-B.txt": {
                        "filename": "file-B.txt",
                        "content": b64encode("test-content-\u212C"),
                    },
                },
                "description": "test-gist",
                "public": True,
                "id": 1,
            }
        ),
        status=200,
    )

    lines = gist_command("content 1")

    assert "file-A.txt:" in lines
    assert "test-content-A" in lines
    assert "file-B.txt:" in lines
    assert "test-content-\u212c" in lines


================================================
FILE: tests/test_cli_parser.py
================================================
import contextlib
import os
import shlex
import unittest.mock

import gist.client
import pytest


@pytest.fixture(autouse=True)
def suppress_stderr():
    with open(os.devnull, "w") as fd:
        with contextlib.redirect_stderr(fd):
            yield


@pytest.mark.parametrize(
    "command",
    [
        "create 'desc'",
        "create --public 'desc'",
        "create --encrypt 'desc'",
        "create --public --encrypt 'desc'",
        "create --public --encrypt 'desc' --filename file1",
        "create --public --encrypt 'desc' file1 file2 file3",
        "create 'desc' --public",
        "create 'desc' --encrypt",
        "create 'desc' --public --encrypt",
    ],
)
def test_cli_parser_gist_create_valid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_create)
    monkeypatch.setattr(gist.client, "handle_gist_create", handler)

    gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize(
    "command",
    [
        "create --public",
        "create --encrypt",
        "create 'desc' --encrypt file1",
        "create --public --encrypt 'desc' --filename file1 file2",
    ],
)
def test_cli_parser_gist_create_invalid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_create)
    monkeypatch.setattr(gist.client, "handle_gist_create", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split(command), config=config)


def test_cli_parser_gist_list_valid(monkeypatch, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_list)
    monkeypatch.setattr(gist.client, "handle_gist_list", handler)

    gist.client.main(argv=shlex.split("list"), config=config)


def test_cli_parser_gist_list_invalid(monkeypatch, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_list)
    monkeypatch.setattr(gist.client, "handle_gist_list", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split("list --no-an-option"), config=config)


@pytest.mark.parametrize("cmd", ["edit", "fork", "info", "files", "archive"])
def test_cli_parser_gist_generic_valid(monkeypatch, cmd, config):
    handler_name = "handle_gist_{}".format(cmd)
    handler_mock = unittest.mock.create_autospec(getattr(gist.client, handler_name))
    monkeypatch.setattr(gist.client, handler_name, handler_mock)

    gist.client.main(argv=shlex.split("{} arg1".format(cmd)), config=config)


@pytest.mark.parametrize("args", ["", "arg1 arg2"])
@pytest.mark.parametrize("cmd", ["edit", "fork", "info", "files", "archive"])
def test_cli_parser_gist_generic_invalid(monkeypatch, cmd, args, config):
    handler_name = "handle_gist_{}".format(cmd)
    handler_mock = unittest.mock.create_autospec(handler_name)
    monkeypatch.setattr(gist.client, handler_name, handler_mock)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split("{} {}".format(cmd, args)), config=config)


@pytest.mark.parametrize("command", ["description id desc", "description id 'desc'"])
def test_cli_parser_gist_description_valid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_description)
    monkeypatch.setattr(gist.client, "handle_gist_description", handler)

    gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize(
    "command",
    [
        "description id",
        "description id foo bar",
    ],
)
def test_cli_parser_gist_description_invalid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_description)
    monkeypatch.setattr(gist.client, "handle_gist_description", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize(
    "command",
    [
        "content id",
        "content id --decrypt",
        "content id file1 --decrypt",
        "content --decrypt id",
        "content --decrypt id file1",
    ],
)
def test_cli_parser_gist_content_valid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_content)
    monkeypatch.setattr(gist.client, "handle_gist_content", handler)

    gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize(
    "command",
    [
        "content",
        "content --decrypt",
        "content id file1 file2 --decrypt",
    ],
)
def test_cli_parser_gist_content_invalid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_content)
    monkeypatch.setattr(gist.client, "handle_gist_content", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize(
    "command",
    [
        "clone id",
        "clone id name",
        "clone id 'long name'",
    ],
)
def test_cli_parser_gist_clone_valid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_clone)
    monkeypatch.setattr(gist.client, "handle_gist_clone", handler)

    gist.client.main(argv=shlex.split(command), config=config)


@pytest.mark.parametrize("command", ["clone", "clone id name1 name2"])
def test_cli_parser_gist_clone_invalid(monkeypatch, command, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_clone)
    monkeypatch.setattr(gist.client, "handle_gist_clone", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split(command), config=config)


def test_cli_parser_gist_version_valid(monkeypatch, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_version)
    monkeypatch.setattr(gist.client, "handle_gist_version", handler)

    gist.client.main(argv=shlex.split("version"), config=config)


def test_cli_parser_gist_version_invalid(monkeypatch, config):
    handler = unittest.mock.create_autospec(gist.client.handle_gist_version)
    monkeypatch.setattr(gist.client, "handle_gist_version", handler)

    with pytest.raises(SystemExit):
        gist.client.main(argv=shlex.split("version arg"), config=config)


================================================
FILE: tests/test_config.py
================================================
import configparser

import gist
import pytest


@pytest.fixture
def config():
    cfg = configparser.ConfigParser()
    cfg.add_section("gist")
    return cfg


def test_get_value_from_command():
    """
    Ensure that values which start with ``!`` are treated as commands and
    return the string printed to stdout by the command, otherwise ensure
    that the value passed to the function is returned.
    """
    assert "magic token" == gist.client.get_value_from_command('!echo "\nmagic token"')
    assert "magic token" == gist.client.get_value_from_command(' !echo "magic token\n"')
    assert "magic token" == gist.client.get_value_from_command("magic token")


def test_get_personal_access_token_missing(config):
    with pytest.raises(gist.client.GistMissingTokenError):
        gist.client.get_personal_access_token(config)


@pytest.mark.parametrize("token", ["", "   "])
def test_get_personal_access_token_empty(config, token):
    config.set("gist", "token", token)
    with pytest.raises(gist.client.GistEmptyTokenError):
        gist.client.get_personal_access_token(config)


@pytest.mark.parametrize("token", ["   123   ", "123abcABC0987"])
def test_get_personal_access_token_valid(config, token):
    config.set("gist", "token", token)
    gist.client.get_personal_access_token(config)


================================================
FILE: tests/test_gist.py
================================================
import base64
import json
import re
import sys

import responses


def b64encode(s):
    """Return the base64 encoding of a string

    To support string encodings other than ascii, the content of a gist needs
    to be uploaded in base64. Because python2.x and python3.x handle string
    differently, it is necessary to be explicit about passing a string into
    b64encode as bytes. This function handles the encoding of the string into
    bytes, and then decodes the resulting bytes into a UTF-8 string, which is
    returned.

    """
    return base64.b64encode(s.encode("utf-8")).decode("utf-8")


@responses.activate
def test_list(gist_api):
    responses.add(
        responses.GET,
        "https://api.github.com/gists",
        body=json.dumps(
            [
                {
                    "id": 1,
                    "description": "test-desc-A",
                    "public": True,
                },
                {
                    "id": 2,
                    "description": "test-desc-\u212C",
                    "public": False,
                },
            ]
        ),
        status=200,
    )

    gists = gist_api.list()

    gistA = gists[0]
    gistB = gists[1]

    assert gistA.id == 1
    assert gistA.desc == "test-desc-A"
    assert gistA.public

    assert gistB.id == 2
    assert gistB.desc == "test-desc-\u212C"
    assert not gistB.public


@responses.activate
def test_list_empty(gist_api):
    responses.add(
        responses.GET,
        "https://api.github.com/gists",
        body="",
        status=200,
    )

    gists = gist_api.list()

    assert len(gists) == 0


@responses.activate
def test_content(gist_api):
    responses.add(
        responses.GET,
        "https://api.github.com/gists/1",
        body=json.dumps(
            {
                "files": {
                    "file-A.txt": {
                        "filename": "file-A.txt",
                        "content": b64encode("test-content-A"),
                    },
                    "file-B.txt": {
                        "filename": "file-B.txt",
                        "content": b64encode("test-content-\u212C"),
                    },
                },
                "description": "test-gist",
                "public": True,
                "id": 1,
            }
        ),
        status=200,
    )

    content = gist_api.content("1")

    assert len(content) == 2
    assert "file-A.txt" in content
    assert "file-B.txt" in content
    assert content["file-A.txt"] == "test-content-A"
    assert content["file-B.txt"] == "test-content-\u212C"


@responses.activate
def test_create(gist_api):
    def request_handler(request):
        data = json.loads(request.body)
        assert len(data["files"]) == 2
        assert "test-file-A" in data["files"]

        content = {k: v["content"] for k, v in data["files"].items()}

        assert content["test-file-A"] == "test-content-A"
        assert content["test-file-B"] == "test-content-\u212C"

        status = 200
        headers = {}
        body = json.dumps({"html_url": "https://gist.github.com/gists/1"})
        return status, headers, body

    responses.add_callback(
        responses.POST,
        "https://api.github.com/gists",
        callback=request_handler,
        content_type="application/json",
    )

    public = True
    desc = "test-desc"
    files = {
        "test-file-A": {"content": "test-content-A"},
        "test-file-B": {"content": "test-content-\u212C"},
    }

    gist_api.create(desc, files, public)


@responses.activate
def test_gnupg_create_from_file(monkeypatch, decrypt, gist_command, tmp_path):
    """
    This test checks that the content from a gist created from a file is
    properly encrypted.

    """

    # This is a work-around for testing with github actions. For some reason, stdin is
    # no a TTY when run from there when it is normally. Hopefull, I can find a bettter
    # solution in the future.
    monkeypatch.setattr(sys.stdin, "isatty", lambda: True)

    def request_handler(request):
        # Decrypt the content of the request and check that it matches the
        # original content.
        body = json.loads(request.body)
        data = list(body["files"].values())
        text = decrypt(data[0]["content"])

        assert u"test-content-\u212C" in text

        status = 200
        headers = {}
        body = json.dumps({"html_url": "https://gist.github.com/gists/1"})

        return status, headers, body

    responses.add_callback(
        responses.POST,
        "https://api.github.com/gists",
        callback=request_handler,
        content_type="application/json",
    )

    # Create a temporary file and write a test message to it
    filename = tmp_path / "gist-test-file.txt"
    with open(filename, "w", encoding="utf-8") as fp:
        fp.write(u"test-content-\u212C\n")

    # It is important to escape the path here to ensure the separators are not stripped
    # on Windows.
    cmd = r'create --encrypt "test-desc" {}'.format(re.escape(str(filename)))

    gist_command(cmd)


@responses.activate
def test_gnupg_content(encrypt, gist_command):
    """
    When encrypted content is received, check to make sure that it can be
    properly decrypted.

    """

    def b64encrypt(content):
        return b64encode(encrypt(content))

    responses.add(
        responses.GET,
        "https://api.github.com/gists/1",
        body=json.dumps(
            {
                "files": {
                    "file-A.txt": {
                        "filename": "file-A.txt",
                        "content": b64encrypt(u"test-content-A"),
                    },
                    "file-B.txt": {
                        "filename": "file-B.txt",
                        "content": b64encrypt(u"test-content-\u212C"),
                    },
                },
                "description": "test-gist",
                "public": True,
                "id": 1,
            }
        ),
        status=200,
    )

    lines = gist_command("content 1 --decrypt")

    assert u"file-A.txt (decrypted):" in lines
    assert u"test-content-A" in lines
    assert u"file-B.txt (decrypted):" in lines
    assert u"test-content-\u212C" in lines


def test_gnupg(encrypt, decrypt):
    """
    Make sure that the basic mechanism put in place for testing the
    encryption used in gist works as expected.

    """
    text = u"this is a message \u212C"
    cypher = encrypt(text)
    plain = decrypt(cypher)

    assert text != cypher
    assert text == plain


================================================
FILE: tox.ini
================================================
[tox]
envlist = py3{6,7,8,9}.*

[testenv]
whitelist_externals = make
commands =
  make lint test
Download .txt
gitextract_stv4va3s/

├── .flake8
├── .github/
│   └── workflows/
│       └── ci.yaml
├── .gitignore
├── LICENSE
├── Makefile
├── README.rst
├── gist/
│   ├── __init__.py
│   ├── client.py
│   ├── gist.py
│   └── version.py
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── share/
│   ├── gist-fzf.bash
│   ├── gist-fzsl.bash
│   ├── gist.bash
│   ├── gist.fish
│   └── gist.zsh
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_cli.py
│   ├── test_cli_parser.py
│   ├── test_config.py
│   └── test_gist.py
└── tox.ini
Download .txt
SYMBOL INDEX (113 symbols across 7 files)

FILE: gist/client.py
  function wrap_stdout_for_unicode (line 158) | def wrap_stdout_for_unicode():
  class GistError (line 170) | class GistError(Exception):
    method __init__ (line 171) | def __init__(self, msg):
  class GistMissingTokenError (line 176) | class GistMissingTokenError(GistError):
  class GistEmptyTokenError (line 180) | class GistEmptyTokenError(GistError):
  class UserError (line 184) | class UserError(Exception):
  class FileInfo (line 188) | class FileInfo(collections.namedtuple("FileInfo", "name content")):
  function terminal_width (line 192) | def terminal_width():
  function elide (line 225) | def elide(txt):
  function get_value_from_command (line 246) | def get_value_from_command(value):
  function get_personal_access_token (line 269) | def get_personal_access_token(config):
  function alternative_editor (line 287) | def alternative_editor(default):
  function environment_editor (line 300) | def environment_editor(default):
  function configuration_editor (line 315) | def configuration_editor(config, default):
  function homedir_config (line 328) | def homedir_config(default):
  function alternative_config (line 339) | def alternative_config(default):
  function xdg_data_config (line 350) | def xdg_data_config(default):
  function environment_config (line 368) | def environment_config(default):
  function load_config_file (line 385) | def load_config_file():
  function handle_gist_list (line 415) | def handle_gist_list(gapi, args, *vargs):
  function handle_gist_edit (line 435) | def handle_gist_edit(gapi, args, *vargs):
  function handle_gist_description (line 448) | def handle_gist_description(gapi, args, *vargs):
  function handle_gist_info (line 462) | def handle_gist_info(gapi, args, *vargs):
  function handle_gist_fork (line 476) | def handle_gist_fork(gapi, args, *vargs):
  function handle_gist_files (line 489) | def handle_gist_files(gapi, args, *vargs):
  function handle_gist_delete (line 503) | def handle_gist_delete(gapi, args, *vargs):
  function handle_gist_archive (line 517) | def handle_gist_archive(gapi, args, *vargs):
  function handle_gist_content (line 530) | def handle_gist_content(gapi, args, config, *vargs):
  function handle_gist_create (line 568) | def handle_gist_create(gapi, args, config, editor, *vargs):
  function handle_gist_clone (line 653) | def handle_gist_clone(gapi, args, *vargs):
  function handle_gist_version (line 666) | def handle_gist_version(gapi, args, *vargs):
  function handle_gist_help (line 678) | def handle_gist_help(gapi, args, *vargs):
  function create_gist_list_parser (line 690) | def create_gist_list_parser(subparser):
  function create_gist_edit_parser (line 701) | def create_gist_edit_parser(subparser):
  function create_gist_description_parser (line 713) | def create_gist_description_parser(subparser):
  function create_gist_info_parser (line 726) | def create_gist_info_parser(subparser):
  function create_gist_fork_parser (line 738) | def create_gist_fork_parser(subparser):
  function create_gist_files_parser (line 750) | def create_gist_files_parser(subparser):
  function create_gist_delete_parser (line 762) | def create_gist_delete_parser(subparser):
  function create_gist_archive_parser (line 774) | def create_gist_archive_parser(subparser):
  function create_gist_content_parser (line 786) | def create_gist_content_parser(subparser):
  function create_gist_create_parser (line 800) | def create_gist_create_parser(subparser):
  function create_gist_clone_parser (line 816) | def create_gist_clone_parser(subparser):
  function create_gist_version_parser (line 829) | def create_gist_version_parser(subparser):
  function create_gist_help_parser (line 840) | def create_gist_help_parser(subparser):
  function create_gist_parser (line 851) | def create_gist_parser():
  function main (line 883) | def main(argv=sys.argv[1:], config=None):

FILE: gist/gist.py
  function pushd (line 17) | def pushd(path):
  class GistInfo (line 24) | class GistInfo(collections.namedtuple("GistInfo", "id public desc")):
  class authenticate (line 28) | class authenticate(object):
    method __init__ (line 34) | def __init__(self, func, method="GET"):
    method get (line 53) | def get(cls, func):
    method post (line 63) | def post(cls, func):
    method patch (line 73) | def patch(cls, func):
    method delete (line 83) | def delete(cls, func):
    method __get__ (line 92) | def __get__(self, instance, owner):
    method __call__ (line 103) | def __call__(self, *args, **kwargs):
  class GistAPI (line 121) | class GistAPI(object):
    method __init__ (line 126) | def __init__(self, token, editor=None):
    method send (line 138) | def send(self, request, stem=None):
    method list (line 164) | def list(self):
    method create (line 238) | def create(self, request, desc, files, public=False):
    method delete (line 257) | def delete(self, request, id):
    method info (line 268) | def info(self, request, id):
    method files (line 282) | def files(self, request, id):
    method content (line 297) | def content(self, request, id):
    method archive (line 320) | def archive(self, request, id):
    method edit (line 346) | def edit(self, request, id):
    method fork (line 372) | def fork(self, request, id):
    method description (line 385) | def description(self, request, id, description):
    method clone (line 397) | def clone(self, id, name=None):

FILE: tests/conftest.py
  function kill_gpg_agent (line 12) | def kill_gpg_agent(homedir):
  function disable_stdout_wrapper (line 41) | def disable_stdout_wrapper(monkeypatch):
  function editor (line 46) | def editor(monkeypatch):
  function gist_api (line 51) | def gist_api():
  function gnupghome (line 56) | def gnupghome():
  function gpg (line 61) | def gpg(gnupghome):
  function fingerprint (line 69) | def fingerprint(gpg):
  function encrypt (line 74) | def encrypt(gpg, fingerprint):
  function decrypt (line 84) | def decrypt(gpg):
  function config (line 95) | def config(gnupghome, fingerprint):
  function gist_command (line 106) | def gist_command(config, capsys):

FILE: tests/test_cli.py
  function b64encode (line 7) | def b64encode(s):
  function test_list (line 22) | def test_list(editor, gist_command):
  function test_content (line 52) | def test_content(editor, gist_command):

FILE: tests/test_cli_parser.py
  function suppress_stderr (line 11) | def suppress_stderr():
  function test_cli_parser_gist_create_valid (line 31) | def test_cli_parser_gist_create_valid(monkeypatch, command, config):
  function test_cli_parser_gist_create_invalid (line 47) | def test_cli_parser_gist_create_invalid(monkeypatch, command, config):
  function test_cli_parser_gist_list_valid (line 55) | def test_cli_parser_gist_list_valid(monkeypatch, config):
  function test_cli_parser_gist_list_invalid (line 62) | def test_cli_parser_gist_list_invalid(monkeypatch, config):
  function test_cli_parser_gist_generic_valid (line 71) | def test_cli_parser_gist_generic_valid(monkeypatch, cmd, config):
  function test_cli_parser_gist_generic_invalid (line 81) | def test_cli_parser_gist_generic_invalid(monkeypatch, cmd, args, config):
  function test_cli_parser_gist_description_valid (line 91) | def test_cli_parser_gist_description_valid(monkeypatch, command, config):
  function test_cli_parser_gist_description_invalid (line 105) | def test_cli_parser_gist_description_invalid(monkeypatch, command, config):
  function test_cli_parser_gist_content_valid (line 123) | def test_cli_parser_gist_content_valid(monkeypatch, command, config):
  function test_cli_parser_gist_content_invalid (line 138) | def test_cli_parser_gist_content_invalid(monkeypatch, command, config):
  function test_cli_parser_gist_clone_valid (line 154) | def test_cli_parser_gist_clone_valid(monkeypatch, command, config):
  function test_cli_parser_gist_clone_invalid (line 162) | def test_cli_parser_gist_clone_invalid(monkeypatch, command, config):
  function test_cli_parser_gist_version_valid (line 170) | def test_cli_parser_gist_version_valid(monkeypatch, config):
  function test_cli_parser_gist_version_invalid (line 177) | def test_cli_parser_gist_version_invalid(monkeypatch, config):

FILE: tests/test_config.py
  function config (line 8) | def config():
  function test_get_value_from_command (line 14) | def test_get_value_from_command():
  function test_get_personal_access_token_missing (line 25) | def test_get_personal_access_token_missing(config):
  function test_get_personal_access_token_empty (line 31) | def test_get_personal_access_token_empty(config, token):
  function test_get_personal_access_token_valid (line 38) | def test_get_personal_access_token_valid(config, token):

FILE: tests/test_gist.py
  function b64encode (line 9) | def b64encode(s):
  function test_list (line 24) | def test_list(gist_api):
  function test_list_empty (line 60) | def test_list_empty(gist_api):
  function test_content (line 74) | def test_content(gist_api):
  function test_create (line 108) | def test_create(gist_api):
  function test_gnupg_create_from_file (line 142) | def test_gnupg_create_from_file(monkeypatch, decrypt, gist_command, tmp_...
  function test_gnupg_content (line 189) | def test_gnupg_content(encrypt, gist_command):
  function test_gnupg (line 230) | def test_gnupg(encrypt, decrypt):
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (86K chars).
[
  {
    "path": ".flake8",
    "chars": 60,
    "preview": "[flake8]\nmax-line-length = 88\nimport-order-style = appnexus\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 705,
    "preview": "name: gist continuous integration\n\non: [pull_request, push]\n\njobs:\n  build:\n    strategy:\n      max-parallel: 16\n      m"
  },
  {
    "path": ".gitignore",
    "chars": 806,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\n"
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Joshua Downer\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "Makefile",
    "chars": 769,
    "preview": "SHELL=/bin/bash\n\nTEST_FILES:=$(wildcard tests/*.py)\nSRC_FILES:=$(wildcard gist/*.py)\n\nREQ_BINS := poetry\n$(foreach bin,$"
  },
  {
    "path": "README.rst",
    "chars": 9985,
    "preview": "==================================================\nGIST\n==================================================\n\n'gist' is a "
  },
  {
    "path": "gist/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "gist/client.py",
    "chars": 25692,
    "preview": "#!/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"
  },
  {
    "path": "gist/gist.py",
    "chars": 11584,
    "preview": "import base64\nimport collections\nimport contextlib\nimport json\nimport os\nimport re\nimport shutil\nimport tarfile\nimport t"
  },
  {
    "path": "gist/version.py",
    "chars": 31,
    "preview": "__version__ = \"0.10.6\"  # noqa\n"
  },
  {
    "path": "pyproject.toml",
    "chars": 1501,
    "preview": "[tool.poetry]\nname = \"python-gist\"\nversion = \"0.10.6\"\ndescription = \"Manage github gists\"\nauthors = [\"Joshua Downer <jos"
  },
  {
    "path": "requirements-dev.txt",
    "chars": 4739,
    "preview": "appdirs==1.4.4; python_version >= \"3.6\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\" and python_v"
  },
  {
    "path": "requirements.txt",
    "chars": 592,
    "preview": "certifi==2021.5.30; python_version >= \"2.7\" and python_full_version < \"3.0.0\" or python_full_version >= \"3.5.0\"\nchardet="
  },
  {
    "path": "share/gist-fzf.bash",
    "chars": 778,
    "preview": "#!/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 comman"
  },
  {
    "path": "share/gist-fzsl.bash",
    "chars": 781,
    "preview": "#!/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 comma"
  },
  {
    "path": "share/gist.bash",
    "chars": 523,
    "preview": "#!/bin/bash\n\n__gist() {\n  local curr=${COMP_WORDS[COMP_CWORD]}\n  local cmd=${COMP_WORDS[1]}\n\n  COMPREPLY=()\n\n  case ${cm"
  },
  {
    "path": "share/gist.fish",
    "chars": 797,
    "preview": "complete -c gist -f -a \"create\"      -d \"Creates a new gist\"\ncomplete -c gist -f -a \"edit\"        -d \"Edit the files in "
  },
  {
    "path": "share/gist.zsh",
    "chars": 950,
    "preview": "#compdef gist\n\n_arguments \\\n  '*:: :->subcmds' && return 0\n\nlocal -a _first_arguments\n_first_arguments=(\n  'list:Print t"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/conftest.py",
    "chars": 2779,
    "preview": "import configparser\nimport errno\nimport shlex\nimport subprocess\n\nimport gist.client\nimport gist.gist\nimport gnupg\nimport"
  },
  {
    "path": "tests/test_cli.py",
    "chars": 2163,
    "preview": "import base64\nimport json\n\nimport responses\n\n\ndef b64encode(s):\n    \"\"\"Return the base64 encoding of a string\n\n    To su"
  },
  {
    "path": "tests/test_cli_parser.py",
    "chars": 6208,
    "preview": "import contextlib\nimport os\nimport shlex\nimport unittest.mock\n\nimport gist.client\nimport pytest\n\n\n@pytest.fixture(autous"
  },
  {
    "path": "tests/test_config.py",
    "chars": 1307,
    "preview": "import configparser\n\nimport gist\nimport pytest\n\n\n@pytest.fixture\ndef config():\n    cfg = configparser.ConfigParser()\n   "
  },
  {
    "path": "tests/test_gist.py",
    "chars": 6552,
    "preview": "import base64\nimport json\nimport re\nimport sys\n\nimport responses\n\n\ndef b64encode(s):\n    \"\"\"Return the base64 encoding o"
  },
  {
    "path": "tox.ini",
    "chars": 97,
    "preview": "[tox]\nenvlist = py3{6,7,8,9}.*\n\n[testenv]\nwhitelist_externals = make\ncommands =\n  make lint test\n"
  }
]

About this extraction

This page contains the full source code of the jdowner/gist GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (78.6 KB), approximately 20.2k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!