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
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
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.