Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# .flake8
[flake8]
select = BLK,C,E,F,I,W
ignore = E203,W503,E501,F401,E731
max-line-length = 88
max-complexity = 10
16 changes: 10 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
language: python
python:
- "3.5"
- "3.6"
- "3.7-dev"
- 3.7
- 3.8
install:
- pip install pipenv
- pipenv install
script: pytest
- pip install nox
env:
- SESSION=tests
jobs:
include:
- python: 3.8
env: SESSION=lint
script: nox -s $SESSION
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ wheel = "*"
ipython = "*"
requests-mock = "*"
pytest = "*"
pandas = "*"
495 changes: 336 additions & 159 deletions Pipfile.lock

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import nox

nox.options.sessions = "lint", "tests"
locations = ["oura", "tests", "samples"]


@nox.session
def tests(session):
args = session.posargs
session.install("pipenv")
session.run("pipenv", "sync")
session.run("pytest", *args)


@nox.session
def lint(session):
args = session.posargs or locations
session.install("flake8", "black", "isort")
session.run("flake8", *args)
session.run("black", "--check", "--diff", *args)
session.run("isort", "-m", "3", "--tc", "--check", "--diff", *args)


@nox.session
def black(session):
args = session.posargs or locations
session.install("black")
session.run("black", *args)


@nox.session
def isort(session):
args = session.posargs or locations
session.install("isort")
session.run("isort", "-m", "3", "--tc", *args)
69 changes: 36 additions & 33 deletions oura/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import json

from requests_oauthlib import OAuth2Session

from . import exceptions
import json
import requests


class OuraOAuth2Client:
"""
Expand All @@ -18,7 +19,7 @@ def __init__(self, client_id, client_secret):

"""
Initialize the client for oauth flow.

:param client_id: The client id from oura portal.
:type client_id: str
:param client_secret: The client secret from oura portal.
Expand All @@ -32,7 +33,6 @@ def __init__(self, client_id, client_secret):
auto_refresh_url=self.TOKEN_BASE_URL,
)


def authorize_endpoint(self, scope=None, redirect_uri=None, **kwargs):
"""
Build the authorization url for a user to click.
Expand All @@ -47,39 +47,43 @@ def authorize_endpoint(self, scope=None, redirect_uri=None, **kwargs):
self.session.redirect_uri = redirect_uri
return self.session.authorization_url(self.AUTHORIZE_BASE_URL, **kwargs)


def fetch_access_token(self, code):
"""
Exchange the auth code for an access and refresh token.
Exchange the auth code for an access and refresh token.

:param code: Authorization code from query string
:type code: str
"""
return self.session.fetch_token(
self.TOKEN_BASE_URL,
code=code,
client_secret = self.client_secret)
self.TOKEN_BASE_URL, code=code, client_secret=self.client_secret
)


class OuraClient:
"""
Use this class for making requests on behalf of a user. If refresh_token and expires_at are supplied,
access_token should be refreshed automatically and passed to the refresh_callback function, along with
other properties in the response.
"""Use this class for making requests on behalf of a user. If refresh_token and
expires_at are supplied, access_token should be refreshed automatically and
passed to the refresh_callback function, along with other response properties.
"""

API_ENDPOINT = "https://api.ouraring.com"
TOKEN_BASE_URL = "https://api.ouraring.com/oauth/token"

def __init__(self, client_id, client_secret=None, access_token=None, refresh_token=None, refresh_callback=None):
def __init__(
self,
client_id,
client_secret=None,
access_token=None,
refresh_token=None,
refresh_callback=None,
):

"""
Initialize the client

:param client_id: The client id from oura portal.
:param client_id: The client id.
:type client_id: str

:param client_secret: The client secret from oura portal. Required for auto refresh.
:param client_secret: The client secret. Required for auto refresh.
:type client_secret: str

:param access_token: Auth token.
Expand All @@ -88,7 +92,7 @@ def __init__(self, client_id, client_secret=None, access_token=None, refresh_tok
:param refresh_token: Use this to renew tokens when they expire
:type refresh_token: str

:param refresh_callback: Method to save the access token, refresh token, expires at
:param refresh_callback: Callback to handle token response
:type refresh_callback: callable

"""
Expand All @@ -97,18 +101,17 @@ def __init__(self, client_id, client_secret=None, access_token=None, refresh_tok
self.client_secret = client_secret
token = {}
if access_token:
token.update({ 'access_token': access_token })
token.update({"access_token": access_token})
if refresh_token:
token.update({ 'refresh_token': refresh_token })
token.update({"refresh_token": refresh_token})

self._session = OAuth2Session(
client_id,
token=token,
auto_refresh_url=self.TOKEN_BASE_URL,
token_updater=refresh_callback
token_updater=refresh_callback,
)


def user_info(self):
"""
Returns information about the logged in user (who the access token was issued for).
Expand All @@ -118,7 +121,6 @@ def user_info(self):
url = "{}/v1/userinfo".format(self.API_ENDPOINT)
return self._make_request(url)


def sleep_summary(self, start=None, end=None):
"""
Get sleep summary for the given date range. See https://cloud.ouraring.com/docs/sleep
Expand All @@ -132,7 +134,6 @@ def sleep_summary(self, start=None, end=None):
url = self._build_summary_url(start, end, "sleep")
return self._make_request(url)


def activity_summary(self, start=None, end=None):
"""
Get activity summary for the given date range. See https://cloud.ouraring.com/docs/activity
Expand All @@ -146,7 +147,6 @@ def activity_summary(self, start=None, end=None):
url = self._build_summary_url(start, end, "activity")
return self._make_request(url)


def readiness_summary(self, start=None, end=None):
"""
Get readiness summary for the given date range. See https://cloud.ouraring.com/docs/readiness
Expand All @@ -160,33 +160,36 @@ def readiness_summary(self, start=None, end=None):
url = self._build_summary_url(start, end, "readiness")
return self._make_request(url)


def _make_request(self, url, data=None, method=None, **kwargs):
data = data or {}
method = method or 'GET'
method = method or "GET"
response = self._session.request(method, url, data=data, **kwargs)
if response.status_code == 401:
self._refresh_token()
response = self._session.request(method, url, data=data, **kwargs)

exceptions.detect_and_raise_error(response)
payload = json.loads(response.content.decode('utf8'))
payload = json.loads(response.content.decode("utf8"))
return payload


def _build_summary_url(self, start, end, datatype):
if start is None:
raise ValueError("Request for {} summary must include start date.".format(datatype))
raise ValueError(
"Request for {} summary must include start date.".format(datatype)
)

url = "{0}/v1/{1}?start={2}".format(self.API_ENDPOINT, datatype, start)
if end:
url = "{0}&end={1}".format(url, end)
return url


def _refresh_token(self):
token = self._session.refresh_token(self.TOKEN_BASE_URL, client_id=self.client_id, client_secret=self.client_secret)
token = self._session.refresh_token(
self.TOKEN_BASE_URL,
client_id=self.client_id,
client_secret=self.client_secret,
)
if self._session.token_updater:
self._session.token_updater(token)

return token
return token
Loading