import json
import requests
from typing import Optional
from .ethwallet import EthWallet
from .endpoints import endPoints
from .errors import Errors
from .schema import Schema


class App():

    def __init__(self, tier, app_private_key, app_handle, debug: bool = False):
        """Initalize the application 
            This lets users initialize the application by providing the tier, application privatekey and application handle
        Args:
            tier  : SANDBOX,PROD etc
            app_private_key : ethereum privat key for the application
            app_handle  : application sila handle (app.silamoney.eth)
        """
        self.session = requests.Session()
        self.tier = tier.lower()
        self.app_private_key = app_private_key
        self.app_handle = app_handle
        self.debug = debug
        self.updateSchema()

    def updateSchema(self):
        """updates schema.py on initialization of app
            This lets users initialize the schema into schema.py for ease of use
        Args:
            None
        """
        endpoint = endPoints["schemaUrl"]
        message = ["header", "entity", "identity", "crypto", "linkAccount"]
        for i in message:
            response = self.get(
                endpoint % i)
            sch = {response["message"]: response}
            Schema.append(sch)

    def getUrl(self):
        """construct the url endpoint to make api calls
        Args:
            app: the initialized applications
        """
        url = endPoints["apiUrl"]
        if self.tier == "prod":
            apiurl = url % "api"
        else:
            apiurl = url % self.tier
        return apiurl

    def post(self, path, payload, header):
        """makes a post request to the sila_apis
        Args:
            path : path to the endpoint being called
            payload : json msg to be posted 
            header  : contains the usersignature and authsignature
        """
        url = self.getUrl()
        endpoint = url + path
        data1 = json.dumps(payload)
        response = self.session.post(
            endpoint,
            data=data1,
            headers=header)

        output = response.json()

        try:
            output['status_code'] = response.status_code
            output['headers'] = dict(response.headers)
        except:
            pass

        return output

    def postFile(self, path, payload, header, fileContents):
        url = self.getUrl()
        endpoint = url + path
        message = json.dumps(payload)
        if type(fileContents) is dict:
            files = fileContents
        else:
            files = {'file': fileContents}
        response = requests.post(
            endpoint,
            data={'data': message},
            headers=header,
            files=files
        )

        output = response.json()

        return output

    def postFileResponse(self, path: str, payload: dict, header: dict) -> requests.Response:
        url = self.getUrl()
        endpoint = url + path
        data = json.dumps(payload)
        response = self.session.post(
            endpoint,
            data=data,
            headers=header
        )

        if (response.status_code == 200):
            return response
        else:
            return response.json()

    def postPlaid(self, url, payload):
        """makes a post request to the sila_apis
        Args:
            path : path to the endpoint being called
            payload : json msg to be posted
            header  : contains the usersignature and authsignature
        """
        content = json.dumps(payload)
        response = self.session.post(
            url,
            data=content,
            headers={
                "Content-Type": "application/json"
            })
        output = response.json()
        return output

    def get(self, path):
        """make a get request using this function
        Args:
            path : path to the endpoint
        """
        endpoint = path
        response = self.session.get(endpoint)
        return checkResponse(response)

    def setHeader(self, msg, key: Optional[str] = None, business_key: Optional[str] = None, content_type: Optional[str] = None):
        """set the application header with usersignature and authsignature
        Args:
            key : ethereum private key for the user
            msg : message being sent should be signed by user
        """
        appsignature = EthWallet.signMessage(msg, self.app_private_key)
        header = {
            "authsignature": appsignature,
            "User-Agent": 'SilaSDK-python/0.2.48'
        }
        if content_type is not None and content_type == 'multipart/form-data':
            pass
        else:
            header["Content-Type"]: 'application/json' if content_type is None else content_type
        if key is not None and len(key.strip()) > 0:
            header["usersignature"] = EthWallet.signMessage(msg, key.lower())
        if business_key is not None and len(business_key.strip()) > 0:
            header["businesssignature"] = EthWallet.signMessage(
                msg, business_key.lower())

        return header

def checkResponse(response):
        """ check if response is in json or not
        Args:
            response : response from the output of other function
        """
        try:
            response_data = response.json()
        except json.decoder.JSONDecodeError:
            response_data = response
        return response_data

 Public https://github.com/Sila-Money/sila-sdk-python/blob/master/silasdk/client.py

Share a link to this review

7.06% issue ratio

R28 Not using dataclass

Dataclasses let you get rid of many boilerplate code, most often the "init hell": def __init__(self, a): self.a = a. With dataclasses, it's all done automatically!

R1 Missing type hints

Type hints help humans and linters (like mypy) to understand what to expect "in" and "out" for a function. Not only it serves as a documentation for others (and you after some time, when the code is wiped from your "brain cache"), but also allows using automated tools to find type errors.

L12 Redundant code / overengineering

This code is not really needed or may be simplified

Suggested change:
api_url = url % ('api' if self.tier == 'prod' else self.tier)
R21 Bad function name

By looking at function name, stranger should roughly understand what the function does. If he doesn't, then the name should be changed.

Why data1?

L33 Request without timeout

Surprise! Python's requests module does not have a timeout by default! So if you don't use the timeout parameter when making a request and if remote resource hangs forever (or is very slow), your code will hang, too. Always use reasonable timeouts. A good value may be 5 seconds.

L9 Bad design

Seems like things could be organized in a better way.

How is this supposed to work? If something goes horribly wrong (btw what can happen here?), silently ignore it?

L38 Not using isinstance()

Idiomatic python type check is if isinstance(type, variable) or if isinstance((type1, type2), variable). Using type(variable) == SomeClass smells because it won't work if variable is a subclass of SomeClass.

L50 Reinventing a wheel

Whatever you want to code, usually someone has already done that. And usually there are some tools to cover your case. And usually you can find them using Google.

Good requests may be not only 200, but 201 etc.

Suggested change:
if response.ok:
L12 Redundant code / overengineering

This code is not really needed or may be simplified

Suggested change:
response = self.session.post(
    url,
    json=payload,
    ...
)
L29 Should be class variable

Key constants of a class should be so-called "class variables" instead of using/defining them inside methods. If you create a child class and want to change those variables, you'll have to rewrite entire methods. With class vars, you only have to overwrite those class vars in child. Example: class MyClass: SOME_VALUE = 1 and class MySubclass(Class): SOME_VALUE = 2

R37 Not using response.raise_for_status()

requests module has built-in method to raise exception on bad response: response.raise_for_status(). If you need to perform some additional actions on bad response, use if not response.ok: ...

This does not check response status, so even if response is 404 NOT FOUND, the code will still try to decode response content.

Suggested change:
response.raise_for_status()
R21 Bad function name

By looking at function name, stranger should roughly understand what the function does. If he doesn't, then the name should be changed.

The checkResponse function doesn't check the response - instead it parses it and returns response data. Funny fact is, the response isn't checked for bad status (so, for example, response can return error 400 with correct json data, and the library won't catch the error).


Create new review request