Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incompatible with aws lambda function. The lambda function only allows customer to touch the "/tmp/" folder. #439

Open
Zishanwang1992 opened this issue Sep 18, 2023 · 5 comments

Comments

@Zishanwang1992
Copy link

home_dir = os.path.expanduser("~")

I attempt to deploy the script to aws lambda function and find out this incompatibility. I finally create a custom version of login function to make it work. Maybe we can add another field of "path" to let user specify when this is used with aws lambda function.

@Mariani-code
Copy link

You can use an ec2 instance in the meantime

@Adelantado
Copy link

Adelantado commented Dec 8, 2023

Sorry to post a opinion / suggestion instead of a suggestion / fix to your issue.
Not sure making personal request to add/modify functions to fit your or anyone else individual needs is a good idea. Lets all be thank full for what we got and let's not start being needy. Need it to work with AWS whatever? ... U seem to be smart enough to get here, put some though into it and figure it out.
And if you did, kudos 2 U, why bother ?

@mm0
Copy link

mm0 commented May 15, 2024

For anyone looking for a solution.. you can try setting the HOME environment variable when running your script/app in order for ~ to expand to the path you specified. You may also have to hope that this doesn't cause issues elsewhere in your code.
eg.

HOME=/tmp/ python script.py

@Adelantado Sometimes it's better not to comment, particularly when there are many people that have created issues for this exact problem. The problem is exactly the opposite of what you mentioned--that the library is setup in a way to fit an individual's needs (original developer) rather than provide flexibility to the users of the library.

This login/authentication mechanism can definitely be improved. It returns a dictionary of values which could theoretically later be used to authenticate API calls without having to login again, yet the library doesn't provide a way for you to later use the dict returned (outside of manually writing them to a pickle file at the expected file location yourself).

A much more flexible approach could've been to provide a way for the user to provide the authentication dict to the login method or have an additional method to set the request session variables from the dict. You can then let the responsibility of storing this dict of values fall onto the user of the library without hardcoding a pickle path/mechanism...say for example if you instead wanted to store in a database or any other datastore.

@Adelantado
Copy link

Dear @mm0: I read you, understand and will disagree with you till my ears falls off.

Stick to the script .... think of it as a wheel:

U have decided, all on your own to "borrow" this "wheel" and used as you please. Now ... if you somehow find yourself stuck and ain't really sure how to make this wheel turn, you have come to the right place. Plenty of smart, dedicated people willing to post, reply, share information and collaborate to fix any new issues that may arise with the normal usage and functionality of the "basic" wheel you borrowed.

So to keep a long story short .... I do not think this is the place to fix or to help fix your or anybody's "specific needs" in relation to any kind of functionality to be developed/implemented outside the basic specs of the wheel. I will further elaborate and add that: if U anna customize the wheel and make it a "fancy" wheel ... just my humble opinion please have a ball, but this ain't the place.

As a final thought I will share, and just in case you still had any doubts, that I can not think of one single reason any kind of AWS integration issue belongs in here.

@griff4594
Copy link

So I read what you guys said, and I understand both arguments, but lets get back to the issue. The issue is the library does not support AWS lambdas, but I have found a fix:

The problem is not just the path of the home directory, but also the fact lambdas aren't always the same every time you fire it. So you can't store it on the lambda at all. You could do a EFS solution, but its easier to modify this authentication file. What I did was make a new authentication file which is below. This still is used in conjunction with this robinhood library, but allows you to used database stored credentials as well.

import getpass
import os
import pickle
import random

from robin_stocks.robinhood.helper import *
from robin_stocks.robinhood.urls import *

def generate_device_token():
"""This function will generate a token used when loggin on.

:returns: A string representing the token.

"""
rands = []
for i in range(0, 16):
    r = random.random()
    rand = 4294967296.0 * r
    rands.append((int(rand) >> ((3 & i) << 3)) & 255)

hexa = []
for i in range(0, 256):
    hexa.append(str(hex(i+256)).lstrip("0x").rstrip("L")[1:])

id = ""
for i in range(0, 16):
    id += hexa[rands[i]]

    if (i == 3) or (i == 5) or (i == 7) or (i == 9):
        id += "-"

return(id)


def respond_to_challenge(challenge_id, sms_code):
"""This function will post to the challenge url.

:param challenge_id: The challenge id.
:type challenge_id: str
:param sms_code: The sms code.
:type sms_code: str
:returns:  The response from requests.

"""
url = challenge_url(challenge_id)
payload = {
    'response': sms_code
}
return(request_post(url, payload))


def login(username=None, password=None, expiresIn=86400, scope='internal', by_sms=True, store_session=True, 
mfa_code=None, pickle_name="", access_token=None, refresh_token=None, token_type='Bearer', device_token=None):
"""This function will effectively log the user into robinhood by getting an
authentication token and saving it to the session header. By default, it
will store the authentication token in a pickle file and load that value
on subsequent logins.

:param username: The username for your robinhood account, usually your email.
    Not required if credentials are already cached and valid.
:type username: Optional[str]
:param password: The password for your robinhood account. Not required if
    credentials are already cached and valid.
:type password: Optional[str]
:param expiresIn: The time until your login session expires. This is in seconds.
:type expiresIn: Optional[int]
:param scope: Specifies the scope of the authentication.
:type scope: Optional[str]
:param by_sms: Specifies whether to send an email(False) or an sms(True)
:type by_sms: Optional[boolean]
:param store_session: Specifies whether to save the log in authorization
    for future log ins.
:type store_session: Optional[boolean]
:param mfa_code: MFA token if enabled.
:type mfa_code: Optional[str]
:param pickle_name: Allows users to name Pickle token file in order to switch
    between different accounts without having to re-login every time.
:returns:  A dictionary with log in information. The 'access_token' keyword contains the access token, and the 'detail' keyword \
contains information on whether the access token was generated or loaded from pickle file.

"""
device_token = generate_device_token()
# Challenge type is used if not logging in with two-factor authentication.
if by_sms:
    challenge_type = "sms"
else:
    challenge_type = "email"

url = login_url()
payload = {
    'client_id': 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS',
    'expires_in': expiresIn,
    'grant_type': 'password',
    'password': password,
    'scope': scope,
    'username': username,
    'challenge_type': challenge_type,
    'device_token': device_token
}

if mfa_code:
    payload['mfa_code'] = mfa_code

# If authentication has been stored in pickle file then load it. Stops login server from being pinged so much.
if access_token and refresh_token:
    # If store_session has been set to false then delete the pickle file, otherwise try to load it.
    # Loading pickle file will fail if the acess_token has expired.
    try:
        set_login_state(True)
        update_session(
            'Authorization', '{0} {1}'.format(token_type, access_token))
        # Try to load account profile to check that authorization token is still valid.
        res = request_get(
            positions_url(), 'pagination', {'nonzero': 'true'}, jsonify_data=False)
        # Raises exception is response code is not 200.
        res.raise_for_status()
        return({'access_token': access_token, 'token_type': token_type,
                'expires_in': expiresIn, 'scope': scope, 'detail': 'logged in using Thriving authentication',
                'backup_code': None, 'refresh_token': refresh_token})
    except:
        print(
            "ERROR: There was an issue loading pickle file. Authentication may be expired - logging in normally.", file=get_output())
        set_login_state(False)
        update_session('Authorization', None)

# Try to log in normally.
if not username:
    username = input("Robinhood username: ")
    payload['username'] = username

if not password:
    password = getpass.getpass("Robinhood password: ")
    payload['password'] = password

data = request_post(url, payload)
print(data)
# Handle case where mfa or challenge is required.
if data:
    if 'mfa_required' in data:
        mfa_token = input("Please type in the MFA code: ")
        payload['mfa_code'] = mfa_token
        res = request_post(url, payload, jsonify_data=False)
        while (res.status_code != 200):
            mfa_token = input(
                "That MFA code was not correct. Please type in another MFA code: ")
            payload['mfa_code'] = mfa_token
            res = request_post(url, payload, jsonify_data=False)
        data = res.json()
    elif 'challenge' in data:
        challenge_id = data['challenge']['id']
        sms_code = input('Enter Robinhood code for validation: ')
        res = respond_to_challenge(challenge_id, sms_code)
        while 'challenge' in res and res['challenge']['remaining_attempts'] > 0:
            sms_code = input('That code was not correct. {0} tries remaining. Please type in another code: '.format(
                res['challenge']['remaining_attempts']))
            res = respond_to_challenge(challenge_id, sms_code)
        update_session(
            'X-ROBINHOOD-CHALLENGE-RESPONSE-ID', challenge_id)
        data = request_post(url, payload)
    # Update Session data with authorization or raise exception with the information present in data.
    if 'access_token' in data:
        token = '{0} {1}'.format(data['token_type'], data['access_token'])
        update_session('Authorization', token)
        set_login_state(True)
        data['detail'] = "logged in with brand new authentication code."
        if store_session:
            return({'token_type': data['token_type'],
            'access_token': data['access_token'],
            'refresh_token': data['refresh_token'],
            'device_token': device_token})
    else:
        raise Exception(data['detail'])
else:
    raise Exception('Error: Trouble connecting to robinhood API. Check internet connection.')
return(data)


@login_required
def logout():
"""Removes authorization from the session header.

:returns: None

"""
set_login_state(False)
update_session('Authorization', None)

`

I now simply use the library like this:

import chalicelib.Bots.Robinhood.authentication as rhAuth

@classmethod
def authenticate(cls, rhObject):
    otp = pyotp.TOTP(rhObject.otp_key).now()
    login = rhAuth.login(username=rhBot.username,
             password=rhObject.password,
             expiresIn=86400,
             mfa_code=otp,
             access_token=rhObject.access_token,
             refresh_token=rhObject.refresh_token,
             device_token=rhObject.device_token,
             token_type='Bearer')
    return login

This allows you to insert the saved credentials each time the session is made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants