Repository: dalenguyen/stockai (language: python)

Issue:
Feat/wealthsimple



Discussion / hints:

Fix patch:
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..296e7e5
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/.travis.yml b/.travis.yml
index cccbb33..d7704ee 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,14 +3,14 @@ language: python
 
 python:
   - "3.7"
-  - "3.7-dev"  # 3.7 development branch
-  - "3.8-dev"  # 3.8 development branch
-  - "nightly"  # nightly build
+  - "3.7-dev" # 3.7 development branch
+  - "3.8-dev" # 3.8 development branch
+  # - "nightly"  # nightly build
 
 install:
-  # - pip install -r requirements.txt
+  - pip install -r requirements.txt
   - python setup.py install
   - pip install coverage
 
 script:
-  - nosetests --with-coverage --cover-package=stockai
\ No newline at end of file
+  - nosetests --with-coverage --cover-package=stockai
diff --git a/README.md b/README.md
index c27a839..723f2e4 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
 
 Python module to get stock data from Yahoo! Finance
 
-*This is an ongoing project. If you have any requests or contributions, please create a [ticket](https://github.com/dalenguyen/stockai/issues)*
+_This is an ongoing project. If you have any requests or contributions, please create a [ticket](https://github.com/dalenguyen/stockai/issues)_
 
 ## Install
 
@@ -15,6 +15,29 @@ From PyPI with pip
 pip install stockai
 ```
 
+## Development
+
+Create a virtual environment
+
+```
+python3 -m venv venv
+source env/bin/activate
+
+pip3 install -r requirements.txt
+```
+
+For MacOS, you may need to use this command in order eto install ciso8601
+
+```
+ARCHFLAGS="-arch x86_64" pip install ciso8601
+```
+
+## Running Tests
+
+```
+python -m unittest tests/*
+```
+
 ## Usage examples
 
 ```python
@@ -33,41 +56,41 @@ print(td.get_currency())
 td.get_historical_prices('2019-01-01', '2019-01-05')
 
 ### The result is a dictionary with ['volumn', 'low', 'open', 'hight', 'close', 'date', 'adjclose']
-{  
-   'volume':[  
+{
+   'volume':[
       3930300,
       5407700,
       5103400
    ],
-   'low':[  
+   'low':[
       67.12000274658203,
       67.12000274658203,
       67.66999816894531
    ],
-   'open':[  
+   'open':[
       67.51000213623047,
       68.11000061035156,
       68.0
    ],
-   'high':[  
+   'high':[
       68.43000030517578,
       68.11000061035156,
       68.1500015258789
    ],
-   'close':[  
+   'close':[
       68.25,
       67.30000305175781,
       67.9800033569336
    ],
-   'date':[  
+   'date':[
       '2019-01-02',
       '2019-01-03',
       '2019-01-04'
    ],
-   'adjclose':[  
+   'adjclose':[
       67.57575225830078,
       66.63513946533203,
       67.30841827392578
    ]
 }
-```
\ No newline at end of file
+```
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..cb351f1
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,25 @@
+# Change log
+
+> **Tags:**
+>
+> - :boom: [Breaking Change]
+> - :eyeglasses: [Spec Compliancy]
+> - :rocket: [New Feature]
+> - :bug: [Bug Fix]
+> - :memo: [Documentation]
+> - :nail_care: [Polish]
+
+---
+
+## [1.0.0] - 2021-02-27
+
+#### - :rocket: [New Feature]
+
+- Restructured projects
+- Added WealthSimple
+
+## [0.0.4] - 2019-04-09
+
+#### - :rocket: [New Feature]
+
+- Retrieve stock price & data from Yahoo! Finance
diff --git a/contributing.md b/contributing.md
new file mode 100644
index 0000000..edfdbb7
--- /dev/null
+++ b/contributing.md
@@ -0,0 +1,3 @@
+# Contributing to StockAI
+
+I would love for you to contribute to this project and help make it better than it is today. I will update the the detail later. For now, just a simple pull request will works.
diff --git a/examples/wealthsimple.py b/examples/wealthsimple.py
new file mode 100644
index 0000000..a62bf68
--- /dev/null
+++ b/examples/wealthsimple.py
@@ -0,0 +1,36 @@
+import sys
+sys.path.append('../stockai')
+
+from stockai import WealthSimple
+
+email: str = ''
+password: str = ''
+
+def prepare_credentails():
+  global email, password
+  print('Prepare credentials')
+  while not email:
+    email = str(input("Enter email: \n>>> "))
+  while not password:
+    password = str(input("Enter password: \n>>> "))
+
+def init():
+  global username, password
+
+  ws = WealthSimple(email, password)
+
+  print('Get me...\n')
+  me = ws.get_me()
+  print(me)
+
+  print('Get accounts\n')
+  accounts = ws.get_accounts()
+  print(accounts)
+
+  print('Get security \n')
+  TSLA = ws.get_security('TSLA')
+  print(TSLA)
+
+prepare_credentails()
+init()
+
diff --git a/requirements.txt b/requirements.txt
index 6715709..78b9bba 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
-ciso8601==2.1.1
-requests==2.21.0
\ No newline at end of file
+ciso8601==2.1.3
+requests==2.25.1
+loguru==0.5.3
diff --git a/setup.py b/setup.py
index 7d06adf..49fbd16 100644
--- a/setup.py
+++ b/setup.py
@@ -3,9 +3,15 @@
 with open("README.md", "r") as fh:
     long_description = fh.read()
 
+# Deploy to PyPI
+#
+# pip install twine
+# python setup.py sdist bdist_wheel
+# twine upload --skip-existing dist/*
+
 setuptools.setup(
     name="stockai",
-    version="0.0.4",
+    version="1.0.0",
     author="Dale Nguyen",
     author_email="dungnq@itbox4vn.com",
     description="Get stock info from Yahoo! Finance",
@@ -13,10 +19,10 @@
     long_description_content_type="text/markdown",
     url="https://github.com/dalenguyen/stockai",
     packages=setuptools.find_packages(),
-    install_requires = [ 'ciso8601', 'requests', ],
+    install_requires = [ 'ciso8601', 'requests', 'loguru' ],
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
         "Operating System :: OS Independent",
     ],
-)
\ No newline at end of file
+)
diff --git a/stockai/__init__.py b/stockai/__init__.py
index 275521a..625425c 100644
--- a/stockai/__init__.py
+++ b/stockai/__init__.py
@@ -2,8 +2,9 @@
 Stockai - get stock information from Yahoo! Finance
 """
 
-__version__ = '0.0.4'
+__version__ = '1.0.0'
 __author__ = 'Dale Nguyen'
 __name__ = 'stockai'
 
-from .stock import Stock
\ No newline at end of file
+from .yahoo.stock import Stock
+from .wealthsimple.stock import WealthSimple
\ No newline at end of file
diff --git a/stockai/wealthsimple/requests.py b/stockai/wealthsimple/requests.py
new file mode 100644
index 0000000..64e5c33
--- /dev/null
+++ b/stockai/wealthsimple/requests.py
@@ -0,0 +1,30 @@
+import requests
+
+class WSAPIRequest:
+    """
+    Handle API requests for WealthSimple
+    """
+
+    def __init__(self, session, WS_URL):
+        self.session = session
+        self.WS_URL = WS_URL
+
+    def request(self, method, endpoint, params=None):
+        url = self.WS_URL + endpoint
+
+        if method == 'POST':
+            return self.post(url, params)
+        elif method == 'GET':
+            return self.get(url, params)
+        else:
+            raise Exception('Invalid request method: {method}')
+
+    def post(self, url, params=None):
+        try:
+            return self.session.post(url, params)
+        except Exception as error:
+            print(error)
+
+    def get(self, url, payload=None):
+      auth = self.session.headers['Authorization']
+      return requests.get(url,  headers = { 'Authorization': auth })
diff --git a/stockai/wealthsimple/stock.py b/stockai/wealthsimple/stock.py
new file mode 100644
index 0000000..2d2654c
--- /dev/null
+++ b/stockai/wealthsimple/stock.py
@@ -0,0 +1,68 @@
+from requests import Session
+from .requests import WSAPIRequest
+from loguru import logger
+
+class WealthSimple():
+  BASE_DOMAIN = 'https://trade-service.wealthsimple.com/'
+
+  def __init__(self, email: str, password: str):
+    self.session = Session()
+    self.WSAPI = WSAPIRequest(self.session, self.BASE_DOMAIN)
+    self.login(email, password)
+
+  def login (self, email: str = None, password: str = None) -> None:
+    if email and password:
+      payload = { "email": email, "password": password, "timeoutMs": 2e4 }
+      response = self.WSAPI.request('POST', 'auth/login', payload)
+
+      # Check of OTP
+      if "x-wealthsimple-otp" in response.headers:
+        TFACode = ''
+        while not TFACode:
+            # Obtain user input and ensure it is not empty
+            TFACode = input('Enter 2FA code: ')
+        payload['otp'] =  TFACode
+        response = self.WSAPI.request('POST', 'auth/login', payload)
+
+      if response.status_code == 401:
+        raise Exception('Invalid Login')
+
+      self.session.headers.update(
+              {"Authorization": response.headers["X-Access-Token"]}
+          )
+      self.session.headers.update(
+              {"refresh_token": response.headers["X-Refresh-Token"]}
+          )
+    else:
+      raise Exception('Missing login credentials')
+
+  def get_me(self):
+    logger.debug('get_me')
+    response = self.WSAPI.request('GET', 'me')
+    logger.debug(f'get_me {response.status_code}')
+
+    if response.status_code == 401:
+      raise Exception('Invalid Access Token')
+    else:
+      return response.json()
+
+  def get_accounts(self) -> list:
+    """
+    Get Wealthsimple Trade Accounts
+    """
+    response = self.WSAPI.request('GET', 'account/list')
+    response = response.json()
+    return response['results']
+
+  def get_security(self, id: str) -> dict:
+      """
+      Get Security Info
+      """
+      logger.debug('get_security')
+      response = self.WSAPI.request('GET', f'securities/{id}')
+      logger.debug(f"get_security {response.status_code}")
+
+      if response.status_code == 401:
+            raise Exception(f'Cannot get security {id}')
+      else:
+          return response.json()
diff --git a/stockai/base.py b/stockai/yahoo/base.py
similarity index 98%
rename from stockai/base.py
rename to stockai/yahoo/base.py
index b86ec76..b52099f 100644
--- a/stockai/base.py
+++ b/stockai/yahoo/base.py
@@ -1,5 +1,5 @@
 from requests import get
-from .utils import timestamp_to_date
+from ..utils import timestamp_to_date
 
 class Base(object):
     def __init__(self, symbol):
diff --git a/stockai/stock.py b/stockai/yahoo/stock.py
similarity index 95%
rename from stockai/stock.py
rename to stockai/yahoo/stock.py
index 65c56ef..9f7d25d 100644
--- a/stockai/stock.py
+++ b/stockai/yahoo/stock.py
@@ -1,5 +1,5 @@
 from .base import Base
-from .utils import date_to_timestamp
+from ..utils import date_to_timestamp
 
 class Stock(Base):
     def __init__(self, symbol):


Test patch:
diff --git a/tests/test.py b/tests/stock.py
similarity index 78%
rename from tests/test.py
rename to tests/stock.py
index 9dec92c..67e9ea9 100644
--- a/tests/test.py
+++ b/tests/stock.py
@@ -11,17 +11,17 @@
 class TestStock(TestCase):
 
     def setUp(self):
-        self.td = Stock('TD.TO')        
+        self.td = Stock('TD.TO')
 
     def test_td_summary_profile(self):
         self.assertEqual(self.td.get_summary_profile()['city'], 'Toronto')
 
     def test_td_financial_data(self):
-        float(self.td.get_price())  
-        self.assertEqual(self.td.get_currency(), 'CAD')    
+        float(self.td.get_price())
+        self.assertEqual(self.td.get_currency(), 'CAD')
 
     def test_td_historical_prices(self):
         dict(self.td.get_historical_prices('2019-01-01', '2019-01-05'))
 
 if __name__ == "__main__":
-    test_main()        
\ No newline at end of file
+    test_main()
diff --git a/tests/wealthsimple.py b/tests/wealthsimple.py
new file mode 100644
index 0000000..8deb228
--- /dev/null
+++ b/tests/wealthsimple.py
@@ -0,0 +1,23 @@
+import sys
+sys.path.append('../stockai')
+
+if sys.version_info < (2, 7):
+    from unittest2 import main as test_main, SkipTest, TestCase
+else:
+    from unittest import main as test_main, SkipTest, TestCase
+
+from stockai import WealthSimple
+
+
+# class TestStock(TestCase):
+
+#     def setUp(self):
+#         self.ws = WealthSimple()
+
+#     def test_td_summary_profile(self):
+#         print(self.ws.test())
+#         self.assertEqual(self.ws.test(), 'test')
+
+
+# if __name__ == "__main__":
+#     test_main()