├── .gitignore ├── LICENSE ├── README.md ├── pysession.py ├── pysession_demo.gif ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Checksum Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Pysession 2 | 3 | Automatically save python interpreter code to a file or secret Gist. You can turn this off for any session. Helpful when you want to share a piece of code you just hacked on the shell or wanted to save it in a file for use later. 4 | 5 | Tested with IPython, Python2 & Python3 default shells. 6 | 7 | ![Pysession demo](./pysession_demo.gif) 8 | 9 | #### Installation steps 10 | 11 | ``` bash 12 | pip install pysession 13 | echo 'export PYTHONSTARTUP=$HOME/.pysession/pysession.py' >> ~/.bashrc 14 | ``` 15 | 16 | If you are using zsh replace `.bashrc` in the above line with `.zshrc` and similarly for any other shell. On macOS, you should append this to `~/.bash_profile` 17 | 18 | #### How to use 19 | 20 | By default, Pysession will record each shell run and save to a Gist. However it can be instructed to turn off recording or save to a file locally instead of GitHub. 21 | 22 | ##### To turn off saving for a session 23 | 24 | ``` python 25 | >>> PySession.off() 26 | ``` 27 | 28 | Alternatively, to persist your choice of not saving sessions for some extended period of time, set an environment variable PYSESSION_SAVE_OFF to True. 29 | `export PYSESSION_SAVE_OFF=True` 30 | 31 | 32 | ##### To turn back on saving for a session 33 | 34 | ``` python 35 | >>> PySession.on() 36 | ``` 37 | 38 | 39 | ##### To save to a local file instead of Gist 40 | 41 | ``` python 42 | >>> PySession.local() 43 | ``` 44 | 45 | To always save your sessions to local file, set an environment variable PYSESSION_SAVE_LOCALLY to True. 46 | `export PYSESSION_SAVE_LOCALLY=True` 47 | 48 | The file is saved with a name `session.py` You can change this by setting the environment variable PYSESSION_FILENAME to your desired filename. 49 | `export PYSESSION_FILENAME=some_file_name.py` 50 | 51 | 52 | -------------------------------------------------------------------------------- /pysession.py: -------------------------------------------------------------------------------- 1 | """A Python history_startup script that persists your work in a Python shell like IDLE or IPython. 2 | 3 | This script saves your work to a secret Gist on Github or to a local file. 4 | """ 5 | 6 | import atexit 7 | import codecs 8 | import datetime 9 | import io 10 | import json 11 | import os 12 | import pickle 13 | import readline 14 | import sys 15 | import webbrowser 16 | 17 | from os.path import expanduser, isfile, join 18 | from sys import stdout 19 | 20 | try: 21 | import urllib2 as urllib 22 | except ImportError: 23 | import urllib.request as urllib 24 | 25 | 26 | DO_NOTHING = '\033[95mNothing to save in this session. Exiting. \n\033[0m' 27 | SAVING_GIST = "\033[95mSaving your session to a secret Gist as '{filename}'. \n\033[0m" 28 | SAVING_FILE = "\033[95mSaving your session to local file '{filename}'.\n\033[0m" 29 | SUCCESS = '\033[95mSaved your session successfully!\n\033[0m' 30 | FAILED = '\033[95mCould not save to Gist. Saving to local file.\n\033[0m' 31 | GIST_DESCRIPTION = 'Exported from a Python Shell using pysession at ' 32 | GIST_API_URL = 'https://api.github.com/gists' 33 | SESSIONS_STORAGE = join(expanduser('~'), '.pysession.pickle') 34 | LAST_GISTS = '\033[95m LAST 5 EXPORTED GISTS: \n\033[0m' 35 | BANNER_GIST = "This interpreter session will be saved to a secret Gist.\n" 36 | BANNER_LOCAL = "This interpreter session will be saved to a local file.\n" 37 | BANNER_OFF = "This interpreter session will not be saved.\n" 38 | BANNER_DISABLE = "You can disable saving this session by typing \033[1mPySession.off()\033[0m\033[95m.\n" 39 | BANNER_ENABLE = "You can enable saving this session type \033[1mPySession.on()\033[0m\033[95m.\n" 40 | BANNER_SWITCH = """\ 41 | To switch between saving the session locally on your disk or 42 | as a secret Gist type \033[1mPySession.local()\033[0m\033[95m resp. \033[1mPySession.gist()\033[0m\033[95m. 43 | """ 44 | 45 | 46 | class PySession(object): 47 | save = True 48 | save_locally = False 49 | is_ipython = False 50 | ipython_history = None 51 | start_index = 0 52 | wrong_code_lines = [] 53 | previous_sessions = [] 54 | 55 | @classmethod 56 | def on(cls): 57 | """Turn on saving for this particular shell session.""" 58 | cls.save = True 59 | 60 | @classmethod 61 | def off(cls): 62 | """Turn off saving for this particular shell session.""" 63 | cls.save = False 64 | 65 | @classmethod 66 | def local(cls): 67 | """Switch to saving the current session to a local file.""" 68 | cls.save = True 69 | cls.save_locally = True 70 | 71 | @classmethod 72 | def gist(cls): 73 | """Switch to saving the current session to a secret gist.""" 74 | cls.save = True 75 | cls.save_locally = False 76 | 77 | @classmethod 78 | def save_to_file(cls, data, filename): 79 | """Saves the session code to a local file in current directory.""" 80 | file_p = io.open(filename, 'wb') 81 | file_p.write(data) 82 | file_p.close() 83 | 84 | @classmethod 85 | def save_to_gist(cls, data, filename): 86 | """Create a secret GitHub Gist with the provided data and filename.""" 87 | date = datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y") 88 | gist = { 89 | 'description': GIST_DESCRIPTION + date, 90 | 'public': False, 91 | 'files': {filename: {'content': data}} 92 | } 93 | 94 | headers = {'Content-Type': 'application/json'} 95 | req = urllib.Request(GIST_API_URL, 96 | json.dumps(gist).encode(), 97 | headers) 98 | response = urllib.urlopen(req) 99 | reader = codecs.getreader("utf-8") 100 | json_response = json.load(reader(response)) 101 | return json_response 102 | 103 | @classmethod 104 | def load_history_urls(cls): 105 | """Loads Gist URLs of past sessions from a pickle file.""" 106 | if isfile(SESSIONS_STORAGE): 107 | PySession.previous_sessions = pickle.load( 108 | io.open(SESSIONS_STORAGE, 'rb')) 109 | 110 | if PySession.previous_sessions: 111 | stdout.write(LAST_GISTS) 112 | for session_url in PySession.previous_sessions: 113 | stdout.write('\t' + session_url + '\n') 114 | 115 | @classmethod 116 | def save_gist_url(cls, url): 117 | PySession.previous_sessions.append(url) 118 | PySession.previous_sessions = PySession.previous_sessions[-5:] 119 | with io.open(SESSIONS_STORAGE, 'wb') as pickle_file: 120 | pickle.dump(PySession.previous_sessions, pickle_file, protocol=2) 121 | 122 | 123 | def init(): 124 | stdout.write("\033[95m----------------------------------------------------------------\n") 125 | if os.getenv('PYSESSION_SAVE_OFF'): 126 | PySession.off() 127 | stdout.write(BANNER_OFF) 128 | elif os.getenv('PYSESSION_SAVE_LOCALLY'): 129 | PySession.local() 130 | stdout.write(BANNER_LOCAL) 131 | else: 132 | stdout.write(BANNER_GIST) 133 | 134 | if os.getenv('PYSESSION_SAVE_OFF'): 135 | stdout.write(BANNER_ENABLE) 136 | else: 137 | stdout.write(BANNER_DISABLE) 138 | 139 | stdout.write(BANNER_SWITCH) 140 | stdout.write("----------------------------------------------------------------\033[0m\n") 141 | 142 | 143 | PySession.load_history_urls() 144 | try: 145 | from IPython import get_ipython 146 | PySession.ipython_history = get_ipython().pt_cli.application.buffer.history 147 | PySession.is_ipython = True 148 | except (ImportError, AttributeError): 149 | pass 150 | 151 | if PySession.is_ipython: 152 | PySession.start_index = len(PySession.ipython_history) 153 | 154 | def custom_hook(shell, etype, evalue, traceback, tb_offset=None): 155 | PySession.wrong_code_lines.append( 156 | len(PySession.ipython_history) - 1) 157 | shell.showtraceback((etype, evalue, traceback), 158 | tb_offset=tb_offset) 159 | 160 | get_ipython().set_custom_exc((Exception,), custom_hook) 161 | else: 162 | readline.add_history('') # A hack for a strange bug in 3 < Py <3.5.2 163 | PySession.start_index = readline.get_current_history_length() + 1 164 | 165 | default_hook = sys.excepthook 166 | 167 | def custom_hook(etype, evalue, traceback): 168 | PySession.wrong_code_lines.append( 169 | readline.get_current_history_length()) 170 | default_hook(etype, evalue, traceback) 171 | 172 | sys.excepthook = custom_hook 173 | 174 | 175 | def process_history(): 176 | """Processes python shell history to an array of code lines""" 177 | end_index = len(PySession.ipython_history) - 1 if PySession.is_ipython \ 178 | else readline.get_current_history_length() 179 | 180 | lines_of_code = [] 181 | for i in range(PySession.start_index, end_index): 182 | if i in PySession.wrong_code_lines: 183 | continue 184 | if PySession.is_ipython: 185 | line = PySession.ipython_history[i] 186 | else: 187 | line = readline.get_history_item(i) 188 | 189 | # remove 'exit' and PySession keywords from code 190 | if line.strip() in ['PySession.local()', 191 | 'PySession.gist()', 192 | 'PySession.off()', 193 | 'exit', 194 | 'exit()']: 195 | continue 196 | lines_of_code.append(line) 197 | 198 | if len( 199 | lines_of_code) > 0 and lines_of_code[-1] != '\n': # adding extra last newline 200 | lines_of_code.append('\n') 201 | 202 | return lines_of_code 203 | 204 | 205 | def before_exit(): 206 | lines_of_code = process_history() 207 | 208 | if not PySession.save or len(lines_of_code) == 0: 209 | stdout.write(DO_NOTHING) 210 | return 211 | 212 | filename = expanduser(os.getenv('PYSESSION_FILENAME', 'session.py')) 213 | 214 | if PySession.save_locally: 215 | stdout.write(SAVING_FILE.format(filename=filename)) 216 | PySession.save_to_file('\n'.join(lines_of_code), filename) 217 | stdout.write(SUCCESS) 218 | return 219 | 220 | try: 221 | stdout.write(SAVING_GIST.format(filename=filename)) 222 | gist_response = PySession.save_to_gist('\n'.join(lines_of_code), filename) 223 | gist_url = gist_response['html_url'] 224 | PySession.save_gist_url(gist_url) 225 | webbrowser.open_new_tab(gist_url) 226 | stdout.write(SUCCESS) 227 | except: 228 | stdout.write(FAILED) 229 | PySession.save_to_file('\n'.join(lines_of_code), filename) 230 | 231 | 232 | init() 233 | atexit.register(before_exit) 234 | -------------------------------------------------------------------------------- /pysession_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FallibleInc/pysession/91f321ade0f9a30f46154e4ed779a95e388b2f42/pysession_demo.gif -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | from os.path import exists, expanduser 4 | from shutil import copyfile 5 | 6 | ROOT = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | if not os.path.exists(expanduser('~') + '/.pysession'): 9 | os.makedirs(expanduser('~') + '/.pysession') 10 | 11 | copyfile(ROOT + '/pysession.py', expanduser('~') + '/.pysession/pysession.py') 12 | 13 | 14 | setup( 15 | name='pysession', 16 | version='0.2', 17 | description='Automatically save python interpreter session code to a file or secret Gist', 18 | author='Fallible', 19 | author_email='hello@fallible.co', 20 | url='https://github.com/FallibleInc/pysession', 21 | download_url='https://github.com/FallibleInc/pysession/tarball/0.2', 22 | py_modules=['pysession'], 23 | install_requires=[], 24 | classifiers=[ 25 | "Development Status :: 5 - Production/Stable", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: Science/Research", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: OS Independent", 30 | "Framework :: IPython", 31 | "Framework :: IDLE", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 2", 34 | "Programming Language :: Python :: 2.6", 35 | "Programming Language :: Python :: 2.7", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.2", 38 | "Programming Language :: Python :: 3.3", 39 | "Programming Language :: Python :: 3.4", 40 | "Programming Language :: Python :: 3.5", 41 | "Programming Language :: Python :: Implementation :: PyPy", 42 | "Topic :: Utilities", 43 | ], 44 | ) 45 | --------------------------------------------------------------------------------