├── venmo ├── __version__.py ├── types.py ├── singletons.py ├── __init__.py ├── cookies.py ├── settings.py ├── user.py ├── payment.py ├── cli.py └── auth.py ├── Makefile ├── .travis.yml ├── .gitignore ├── publish.sh ├── Pipfile ├── tests └── user_test.py ├── setup.py └── README.rst /venmo/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.12.0' 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install pipenv --upgrade 3 | pipenv install --dev --skip-lock 4 | 5 | test: 6 | py.test --disable-socket tests 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | - "nightly" 6 | install: 7 | - make init 8 | script: 9 | - flake8 10 | - make test 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | 3 | # Setuptools distribution folder. 4 | /dist/ 5 | 6 | # Python egg metadata, regenerated from source files by setuptools. 7 | .eggs 8 | /*.egg-info 9 | build 10 | 11 | __pycache__ 12 | *.pyc 13 | .cache 14 | -------------------------------------------------------------------------------- /venmo/types.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Argument types 3 | ''' 4 | 5 | import argparse 6 | 7 | 8 | def positive_float(s): 9 | if float(s) <= 0: 10 | raise argparse.ArgumentTypeError('{} is not positive'.format(s)) 11 | return float(s) 12 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | rm -rf dist/* 5 | 6 | echo "Building Source and Wheel (universal) distribution..." 7 | python setup.py sdist bdist_wheel --universal 8 | 9 | echo "Uploading the package to PyPi via Twine..." 10 | twine upload dist/* 11 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | 3 | url = "https://pypi.python.org/simple" 4 | verify_ssl = true 5 | name = "pypi" 6 | 7 | 8 | [packages] 9 | 10 | venmo = {path = ".", editable = true} 11 | 12 | 13 | [dev-packages] 14 | 15 | flake8 = "*" 16 | pytest = "*" 17 | pytest-socket = "*" 18 | mock = "*" 19 | -------------------------------------------------------------------------------- /venmo/singletons.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | 3 | from requests import Session 4 | 5 | import venmo 6 | 7 | _session = None 8 | 9 | 10 | def _save_cookies(): 11 | venmo.cookies.save(session().cookies) 12 | 13 | 14 | def session(): 15 | global _session 16 | if not _session: 17 | _session = Session() 18 | _session.cookies = venmo.cookies.load() 19 | atexit.register(_save_cookies) 20 | return _session 21 | -------------------------------------------------------------------------------- /venmo/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .__version__ import __version__ # noqa: F401 4 | 5 | logging.basicConfig(level=logging.INFO, 6 | format='%(asctime)s %(name)s: %(levelname)s %(message)s') 7 | 8 | logging.getLogger('requests').setLevel(logging.WARNING) 9 | 10 | 11 | from . import ( # noqa: F401 12 | auth, 13 | cli, 14 | cookies, 15 | payment, 16 | settings, 17 | singletons, 18 | types, 19 | user 20 | ) 21 | -------------------------------------------------------------------------------- /venmo/cookies.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | 4 | import requests.cookies 5 | 6 | import venmo 7 | 8 | 9 | def save(requests_cookiejar): 10 | try: 11 | os.makedirs(os.path.dirname(venmo.settings.COOKIES_FILE)) 12 | except OSError: 13 | pass # It's okay if directory already exists 14 | with open(venmo.settings.COOKIES_FILE, 'wb') as f: 15 | pickle.dump(requests_cookiejar, f, -1) 16 | 17 | 18 | def load(): 19 | try: 20 | with open(venmo.settings.COOKIES_FILE, 'rb') as f: 21 | return pickle.load(f) 22 | except IOError: 23 | # No cookies 24 | return requests.cookies.RequestsCookieJar() 25 | -------------------------------------------------------------------------------- /venmo/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # OAuth 5 | CLIENT_ID = '2667' 6 | CLIENT_SECRET = 'srDrmU3yf452HuFF63HqHEt25pa5DexZ' 7 | 8 | # Paths 9 | DOT_VENMO = os.path.join(os.path.expanduser('~'), '.venmo') 10 | CREDENTIALS_FILE = os.path.join(DOT_VENMO, 'credentials') 11 | COOKIES_FILE = os.path.join(DOT_VENMO, 'cookies') 12 | 13 | # URLs 14 | ACCESS_TOKEN_URL = 'https://api.venmo.com/v1/oauth/access_token' 15 | AUTHORIZATION_URL = 'https://api.venmo.com/v1/oauth/authorize' 16 | PAYMENTS_URL = 'https://api.venmo.com/v1/payments' 17 | TWO_FACTOR_URL = 'https://venmo.com/api/v5/two_factor/token' 18 | TWO_FACTOR_AUTHORIZATION_URL = 'https://venmo.com/login' 19 | USERS_URL = 'https://api.venmo.com/v1/users' 20 | -------------------------------------------------------------------------------- /tests/user_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import venmo 4 | 5 | if sys.version_info <= (3, 3): 6 | import mock 7 | else: 8 | from unittest import mock 9 | 10 | 11 | @mock.patch.object(venmo.user, 'search') 12 | def test_id_from_username_case_insensitive(mock_search): 13 | # Mock out Venmo API response. It is case insensitive. 14 | mock_search.return_value = [ 15 | { 16 | "id": "975376293560320029", 17 | "username": "zackhsi", 18 | "display_name": "Zack Hsi", 19 | "profile_picture_url": "https://venmopics.appspot.com/u/v2/f/172a1500-63f5-4d78-b15a-a06dc9c0ad82" # noqa 20 | }, 21 | ] 22 | 23 | ids = [ 24 | venmo.user.id_from_username('zackhsi'), 25 | venmo.user.id_from_username('Zackhsi'), 26 | venmo.user.id_from_username('ZACKHSI'), 27 | ] 28 | # Assert that all return ids. 29 | assert all(ids) 30 | # Assert that the same id is returned. 31 | assert len(set(ids)) == 1 32 | -------------------------------------------------------------------------------- /venmo/user.py: -------------------------------------------------------------------------------- 1 | ''' 2 | User module. 3 | ''' 4 | import json 5 | 6 | import requests 7 | 8 | import venmo 9 | 10 | 11 | def id_from_username(username): 12 | for u in search(username): 13 | if u['username'].lower() == username.lower(): 14 | return u['id'] 15 | return None 16 | 17 | 18 | def print_search(query): 19 | print(json.dumps(search(query), indent=4)) 20 | 21 | 22 | def search(query): 23 | venmo.auth.ensure_access_token() 24 | response = requests.get( 25 | venmo.settings.USERS_URL, 26 | params={ 27 | 'limit': 5, 28 | 'query': query, 29 | }, 30 | headers={ 31 | 'Authorization': 'Bearer {}'.format(venmo.auth.get_access_token()) 32 | }, 33 | ) 34 | users = response.json()['data'] 35 | results = [] 36 | for u in users: 37 | results.append({ 38 | 'id': u['id'], 39 | 'username': u['username'], 40 | 'display_name': u['display_name'], 41 | 'profile_picture_url': u['profile_picture_url'], 42 | }) 43 | return results 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | import re 4 | 5 | from setuptools import setup 6 | 7 | 8 | def fpath(name): 9 | return os.path.join(os.path.dirname(__file__), name) 10 | 11 | 12 | def read(fname): 13 | return codecs.open(fpath(fname), encoding='utf-8').read() 14 | 15 | 16 | def grep(attrname, filename): 17 | pattern = r"{0}\W*=\W*'([^']+)'".format(attrname) 18 | file_text = read(fpath(filename)) 19 | strval, = re.findall(pattern, file_text) 20 | return strval 21 | 22 | 23 | setup( 24 | name='venmo', 25 | version=grep('__version__', 'venmo/__version__.py'), 26 | description='Venmo CLI', 27 | long_description=read(fpath('README.rst')), 28 | url='http://github.com/zackhsi/venmo', 29 | author='Zack Hsi', 30 | author_email='zackhsi@gmail.com', 31 | license='MIT', 32 | packages=['venmo'], 33 | install_requires=[ 34 | 'requests>=2.9.1', 35 | ], 36 | entry_points={ 37 | 'console_scripts': [ 38 | 'venmo = venmo.cli:main', 39 | ], 40 | }, 41 | classifiers=[ 42 | 'Programming Language :: Python :: 2.7', 43 | 'Programming Language :: Python :: 3.6', 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/venmo.svg 2 | :target: https://pypi.python.org/pypi/venmo 3 | 4 | .. image:: https://travis-ci.org/zackhsi/venmo.svg?branch=master 5 | :target: https://travis-ci.org/zackhsi/venmo 6 | 7 | Venmo 8 | ===== 9 | 10 | Pay or charge people on the command line! 11 | 12 | :: 13 | 14 | $ venmo pay @zackhsi 23.19 "Thanks for the beer <3" 15 | $ venmo charge 19495551234 23.19 "That beer wasn't free!" 16 | 17 | Installation 18 | ------------ 19 | 20 | ``venmo`` can be installed via ``pip``. 21 | 22 | :: 23 | 24 | $ pip install venmo 25 | 26 | Setup 27 | ----- 28 | Set up venmo by running: 29 | 30 | :: 31 | 32 | $ venmo configure 33 | 34 | > Venmo email [None]: zackhsi@gmail.com 35 | > Venmo password [None]: 36 | > Verification code: 908126 # for 2 factor authentication 37 | 38 | That's it! 39 | 40 | Contributing 41 | ------------ 42 | Pull requests welcome! To get started, first clone the repository: 43 | 44 | :: 45 | 46 | $ git clone git@github.com:zackhsi/venmo.git 47 | 48 | Create a virtualenv containing an editable installation of venmo, plus 49 | development dependencies: 50 | 51 | :: 52 | 53 | $ make init 54 | 55 | Activate the virtualenv: 56 | 57 | :: 58 | 59 | $ pipenv shell 60 | 61 | Run tests: 62 | 63 | :: 64 | 65 | $ make test 66 | -------------------------------------------------------------------------------- /venmo/payment.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Payment 3 | ''' 4 | 5 | import logging 6 | import sys 7 | import requests 8 | 9 | import venmo 10 | 11 | logger = logging.getLogger('venmo.payment') 12 | 13 | 14 | def pay(user, amount, note): 15 | _pay_or_charge(user, amount, note) 16 | 17 | 18 | def charge(user, amount, note): 19 | amount = -amount 20 | _pay_or_charge(user, amount, note) 21 | 22 | 23 | def _pay_or_charge(user, amount, note): 24 | venmo.auth.ensure_access_token() 25 | access_token = venmo.auth.get_access_token() 26 | 27 | data = { 28 | 'note': note, 29 | 'amount': amount, 30 | 'access_token': access_token, 31 | 'audience': 'private', 32 | } 33 | if user.startswith('@'): 34 | username = user[1:] 35 | user_id = venmo.user.id_from_username(username) 36 | if not user_id: 37 | logger.error('Could not find user @{}'.format(username)) 38 | return 39 | data['user_id'] = user_id 40 | else: 41 | data['phone'] = user 42 | 43 | response = requests.post( 44 | venmo.settings.PAYMENTS_URL, 45 | json=data 46 | ) 47 | data = response.json() 48 | 49 | try: 50 | response.raise_for_status() 51 | except requests.exceptions.HTTPError as e: 52 | error_message = 'received {} from Venmo'.format(e.response.status_code) 53 | if 'error' in data: 54 | message = data['error']['message'] 55 | error_message += ': "{}"'.format(message) 56 | print(error_message) 57 | sys.exit(1) 58 | 59 | payment = data['data']['payment'] 60 | target = payment['target'] 61 | payment_action = payment['action'] 62 | if payment_action == 'charge': 63 | payment_action = 'charged' 64 | if payment_action == 'pay': 65 | payment_action = 'paid' 66 | amount = payment['amount'] 67 | if target['type'] == 'user': 68 | user = '{first_name} {last_name}'.format( 69 | first_name=target['user']['first_name'], 70 | last_name=target['user']['last_name'], 71 | ) 72 | else: 73 | user = target[target['type']], 74 | note = payment['note'] 75 | print('Successfully {payment_action} {user} ${amount:.2f} for "{note}"' 76 | .format( 77 | payment_action=payment_action, 78 | user=user, 79 | amount=amount, 80 | note=note, 81 | )) 82 | -------------------------------------------------------------------------------- /venmo/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Venmo CLI. 5 | 6 | Pay or charge people via the Venmo API: 7 | 8 | venmo pay @zackhsi 23.19 'Thanks for the beer <3' 9 | venmo charge 19495551234 23.19 'That beer wasn't free!' 10 | ''' 11 | 12 | import argparse 13 | import os 14 | import sys 15 | from datetime import datetime 16 | 17 | import venmo 18 | 19 | 20 | def status(): 21 | '''Print out system status 22 | 23 | $ venmo status 24 | Version 0.3.2 25 | Credentials (updated 2016-01-26 19:48): 26 | User: youremailaddress 27 | Token: youraccesstoken 28 | ''' 29 | print('\n'.join([_version(), _credentials()])) 30 | 31 | 32 | def _version(): 33 | return 'Version {}'.format(venmo.__version__) 34 | 35 | 36 | def _credentials(): 37 | try: 38 | updated_at = os.path.getmtime(venmo.settings.CREDENTIALS_FILE) 39 | updated_at = datetime.fromtimestamp(updated_at) 40 | updated_at = updated_at.strftime('%Y-%m-%d %H:%M') 41 | return '''Credentials (updated {updated_at}): 42 | User: {user} 43 | Token: {token}'''.format(updated_at=updated_at, 44 | user=venmo.auth.get_username(), 45 | token=venmo.auth.get_access_token()) 46 | except OSError: 47 | return 'No credentials' 48 | 49 | 50 | def parse_args(): 51 | parser = argparse.ArgumentParser( 52 | description=__doc__, 53 | formatter_class=argparse.RawDescriptionHelpFormatter, 54 | ) 55 | subparsers = parser.add_subparsers() 56 | 57 | for action in ['pay', 'charge']: 58 | subparser = subparsers.add_parser(action, 59 | help='{} someone'.format(action)) 60 | subparser.add_argument( 61 | 'user', 62 | help='who to {}, either phone or username'.format(action), 63 | ) 64 | subparser.add_argument('amount', type=venmo.types.positive_float, 65 | help='how much to pay or charge') 66 | subparser.add_argument('note', help='what the request is for') 67 | subparser.set_defaults(func=getattr(venmo.payment, action)) 68 | 69 | parser_configure = subparsers.add_parser('configure', 70 | help='set up credentials') 71 | parser_configure.set_defaults(func=venmo.auth.configure) 72 | 73 | parser_search = subparsers.add_parser('search', help='search users') 74 | parser_search.add_argument('query', help='search query') 75 | parser_search.set_defaults(func=venmo.user.print_search) 76 | 77 | parser_status = subparsers.add_parser('status', help='get status') 78 | parser_status.set_defaults(func=status) 79 | 80 | parser_reset = subparsers.add_parser('reset', help='reset saved data') 81 | parser_reset.set_defaults(func=venmo.auth.reset) 82 | 83 | parser.add_argument('-v', '--version', action='version', 84 | version='%(prog)s ' + venmo.__version__) 85 | 86 | if len(sys.argv) == 1: 87 | sys.argv.append('-h') 88 | args = parser.parse_args() 89 | func = args.func 90 | del args.func 91 | func(**vars(args)) 92 | 93 | 94 | def main(): 95 | try: 96 | parse_args() 97 | except KeyboardInterrupt: 98 | print('') 99 | 100 | 101 | if __name__ == '__main__': 102 | main() 103 | -------------------------------------------------------------------------------- /venmo/auth.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Authentication 3 | ''' 4 | 5 | import getpass 6 | import logging 7 | import os 8 | import os.path 9 | import re 10 | import shutil 11 | import xml.etree.ElementTree as ET 12 | 13 | import venmo 14 | 15 | # Python 2.x fixes 16 | try: 17 | import configparser 18 | except ImportError: 19 | import ConfigParser as configparser 20 | 21 | try: 22 | from urllib.parse import urlencode 23 | except ImportError: 24 | from urllib import urlencode 25 | 26 | try: 27 | # pylint: disable=redefined-builtin 28 | input = raw_input 29 | except NameError: 30 | pass 31 | 32 | 33 | logger = logging.getLogger('venmo.auth') 34 | 35 | 36 | def configure(): 37 | '''Get venmo ready to make payments. 38 | 39 | First, set username and password. If those change, then get access token. 40 | If that fails, unset username and password. 41 | 42 | Return whether or not we save an access token. 43 | ''' 44 | # Update credentials 45 | credentials = update_credentials() 46 | if not credentials: 47 | return False 48 | else: 49 | email, password = credentials 50 | 51 | # Log in to Auto 52 | success = submit_credentials(email, password) 53 | if not success: 54 | return False 55 | else: 56 | redirect_url, csrftoken2 = success 57 | 58 | # 2FA is expected because issue #23 59 | if 'two-factor' not in redirect_url: 60 | logger.error('invalid credentials') 61 | return False 62 | 63 | # Write email password 64 | config = read_config() 65 | config.set(configparser.DEFAULTSECT, 'email', email) 66 | config.set(configparser.DEFAULTSECT, 'password', password) 67 | write_config(config) 68 | 69 | # Do 2FA 70 | access_token = two_factor(redirect_url, csrftoken2, email, password) 71 | if not access_token: 72 | return False 73 | 74 | # Write access token 75 | config = read_config() 76 | config.set(configparser.DEFAULTSECT, 'access_token', access_token) 77 | write_config(config) 78 | return True 79 | 80 | 81 | def two_factor(redirect_url, csrftoken2, email, password): 82 | '''Do the two factor auth dance. 83 | 84 | Return access_token or False. 85 | ''' 86 | logger.info('Sending SMS verification ...') 87 | 88 | # Get two factor page 89 | response = venmo.singletons.session().get(redirect_url) 90 | 91 | # Send SMS 92 | secret = extract_otp_secret(response.text) 93 | headers = {'Venmo-Otp-Secret': secret} 94 | data = { 95 | 'via': 'sms', 96 | 'csrftoken2': csrftoken2, 97 | } 98 | response = venmo.singletons.session().post( 99 | venmo.settings.TWO_FACTOR_URL, 100 | json=data, 101 | headers=headers, 102 | ) 103 | assert response.status_code == 200, 'Post to 2FA failed' 104 | assert response.json()['data']['status'] == 'sent', 'SMS did not send' 105 | 106 | # Prompt verification code 107 | verification_code = input('Verification code: ') 108 | if not verification_code: 109 | logger.error('verification code required') 110 | return False 111 | 112 | # Submit verification code 113 | data = { 114 | 'csrftoken2': csrftoken2, 115 | 'return_json': 'true', 116 | 'password': password, 117 | 'phoneEmailUsername': email, 118 | 'token': verification_code, 119 | } 120 | response = venmo.singletons.session().post( 121 | venmo.settings.TWO_FACTOR_AUTHORIZATION_URL, 122 | json=data, 123 | allow_redirects=False, 124 | ) 125 | if response.status_code != 200: 126 | logger.error('verification code failed') 127 | return False 128 | 129 | # Retrieve access token 130 | access_token = response.json()['access_token'] 131 | return access_token 132 | 133 | 134 | def extract_otp_secret(text): 135 | pattern = re.compile(r'"secret":"(\w*)"') 136 | for line in text.splitlines(): 137 | match = pattern.search(line) 138 | return match.group(1) 139 | raise Exception('msg="Could not extract data-otp-secret"') 140 | 141 | 142 | def retrieve_access_token(code): 143 | data = { 144 | 'client_id': venmo.settings.CLIENT_ID, 145 | 'client_secret': venmo.settings.CLIENT_SECRET, 146 | 'code': code, 147 | } 148 | response = venmo.singletons.session().post(venmo.settings.ACCESS_TOKEN_URL, 149 | data) 150 | response_dict = response.json() 151 | access_token = response_dict['access_token'] 152 | return access_token 153 | 154 | 155 | def _authorization_url(): 156 | scopes = [ 157 | 'make_payments', 158 | 'access_feed', 159 | 'access_profile', 160 | 'access_email', 161 | 'access_phone', 162 | 'access_balance', 163 | 'access_friends', 164 | ] 165 | params = { 166 | 'client_id': venmo.settings.CLIENT_ID, 167 | 'scope': ' '.join(scopes), 168 | 'response_type': 'code', 169 | } 170 | return '{authorization_url}?{params}'.format( 171 | authorization_url=venmo.settings.AUTHORIZATION_URL, 172 | params=urlencode(params) 173 | ) 174 | 175 | 176 | def _filter_tag(input_xml, tag): 177 | '''Filter out the script so we can parse the xml.''' 178 | output_lines = [] 179 | inside = False 180 | for line in input_xml.splitlines(): 181 | if '<{tag}>'.format(tag=tag) in line: 182 | inside = True 183 | if not inside: 184 | output_lines.append(line) 185 | if ''.format(tag=tag) in line: 186 | inside = False 187 | return '\n'.join(output_lines) 188 | 189 | 190 | def update_credentials(): 191 | '''Save username and password to config file. 192 | 193 | Entering nothing keeps the current credentials. Returns whether or not 194 | the credentials changed. 195 | ''' 196 | # Read old credentials 197 | config = read_config() 198 | try: 199 | old_email = config.get(configparser.DEFAULTSECT, 'email') 200 | except configparser.NoOptionError: 201 | old_email = '' 202 | try: 203 | old_password = config.get(configparser.DEFAULTSECT, 'password') 204 | except configparser.NoOptionError: 205 | old_password = '' 206 | 207 | # Prompt new credentials 208 | email = input('Venmo email [{}]: ' 209 | .format(old_email if old_email else None)) 210 | password = getpass.getpass(prompt='Venmo password [{}]: ' 211 | .format('*' * 10 if old_password else None)) 212 | email = email or old_email 213 | password = password or old_password 214 | 215 | incomplete = not email or not password 216 | if incomplete: 217 | logger.warn('credentials incomplete') 218 | return False 219 | 220 | return email, password 221 | 222 | 223 | def submit_credentials(email, password): 224 | # Get and parse authorization webpage xml and form 225 | response = venmo.singletons.session().get(_authorization_url()) 226 | authorization_page_xml = response.text 227 | filtered_xml = _filter_tag(authorization_page_xml, 'script') 228 | filtered_xml = _filter_tag(filtered_xml, 'head') 229 | root = ET.fromstring(filtered_xml) 230 | form = root.find('.//form') 231 | for child in form: 232 | if child.attrib.get('name') == 'csrftoken2': 233 | csrftoken2 = child.attrib['value'] 234 | if child.attrib.get('name') == 'auth_request': 235 | auth_request = child.attrib['value'] 236 | if child.attrib.get('name') == 'web_redirect_url': 237 | web_redirect_url = child.attrib['value'] 238 | 239 | # Submit form 240 | data = { 241 | 'username': email, 242 | 'password': password, 243 | 'web_redirect_url': web_redirect_url, 244 | 'csrftoken2': csrftoken2, 245 | 'auth_request': auth_request, 246 | 'grant': 1, 247 | } 248 | response = venmo.singletons.session().post( 249 | venmo.settings.AUTHORIZATION_URL, 250 | json=data, 251 | allow_redirects=False, 252 | ) 253 | 254 | if response.status_code != 302: 255 | logger.error('expecting a redirect') 256 | return False 257 | 258 | redirect_url = response.headers['location'] 259 | return redirect_url, csrftoken2 260 | 261 | 262 | def get_username(): 263 | config = read_config() 264 | try: 265 | return config.get(configparser.DEFAULTSECT, 'email') 266 | except configparser.NoOptionError: 267 | return None 268 | 269 | 270 | def get_password(): 271 | config = read_config() 272 | return config.get(configparser.DEFAULTSECT, 'password') 273 | 274 | 275 | def get_access_token(): 276 | config = read_config() 277 | try: 278 | return config.get(configparser.DEFAULTSECT, 'access_token') 279 | except configparser.NoOptionError: 280 | return None 281 | 282 | 283 | def ensure_access_token(): 284 | while not get_access_token(): 285 | logger.warn('No access token. Configuring ...') 286 | configure() 287 | 288 | 289 | def read_config(): 290 | config = configparser.RawConfigParser() 291 | config.read(venmo.settings.CREDENTIALS_FILE) 292 | return config 293 | 294 | 295 | def write_config(config): 296 | try: 297 | os.makedirs(os.path.dirname(venmo.settings.CREDENTIALS_FILE)) 298 | except OSError: 299 | pass # It's okay if directory already exists 300 | with open(venmo.settings.CREDENTIALS_FILE, 'w') as configfile: 301 | config.write(configfile) 302 | 303 | 304 | def reset(): 305 | '''rm -rf ~/.venmo''' 306 | shutil.rmtree(venmo.settings.DOT_VENMO) 307 | --------------------------------------------------------------------------------