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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ __pycache__
build
*.egg-info
.tox
.nox
docs/_build/
data
test_token.json
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ jobs:
include:
- python: 3.8
env: SESSION=lint
- python: 3.8
env: SESSION=docs
script: nox -s $SESSION
3 changes: 0 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ verify_ssl = true

[packages]
requests-oauthlib = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
flask = "*"
twine = "*"
wheel = "*"
ipython = "*"
requests-mock = "*"
pytest = "*"
Expand Down
284 changes: 101 additions & 183 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ Client
:synopsis: Probably the best way to call the Oura API using python.
:members:

.. automodule:: oura.client_pandas
:synopsis: Probably the best way to call the Oura API using python.
:members:
17 changes: 16 additions & 1 deletion docs/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,19 @@ In following the standard flow, you would have some code under your `/callback`
token_response = auth_client.fetch_access_token(code=code)


Now you are ready to make authenticated API requests. Please use this power responsibly.
Now you are ready to make authenticated API requests. Please use this power responsibly.

Personal Access Token
=====================

You can also access your own data using a personal_access_token - get one from
the cloud portal and save the value somewhere, like an environment variable. Or
somewhere else, it's your token anyway. Then just pass it to a new
`OuraClient` instance and you'll be ready to go. See what I mean::

import os
from oura import OuraClient
my_token = os.getenv('MY_TOKEN')
client = OuraClient(personal_access_token=my_token)
who_am_i = client.user_info()

2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# -- Project information -----------------------------------------------------

project = 'python-oura'
copyright = '2019, Jon Hagg'
copyright = '2020, Jon Hagg'
author = 'Jon Hagg'

# The short X.Y version
Expand Down
33 changes: 7 additions & 26 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
alabaster==0.7.12
Babel==2.6.0
certifi==2018.11.29
chardet==3.0.4
Click==7.0
docutils==0.14
Flask==1.0.2
idna==2.8
imagesize==1.1.0
itsdangerous==1.1.0
Jinja2==2.10.1
MarkupSafe==1.1.0
oauthlib==2.1.0
packaging==18.0
Pygments==2.3.1
pyparsing==2.3.0
pytz==2018.7
requests==2.21.0
requests-oauthlib==1.0.0
six==1.12.0
snowballstemmer==1.2.1
Sphinx==1.8.3
sphinx-rtd-theme==0.4.2
sphinxcontrib-websupport==1.1.0
urllib3==1.24.2
Werkzeug==0.15.3
sphinx
sphinx-rtd-theme
flask
pandas
pytest
requests-mock
requests-oauthlib
2 changes: 1 addition & 1 deletion docs/summaries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Daily summaries
********************************

Oura's API is based on the idea of daily summaries. For each kind of data (sleep, activity, readiness)
Oura's API is based on the idea of daily summaries. For each kind of data (sleep, activity, readiness, bedtime)
there is an endpoint which will return summaries for one or more day. They take a start and end date in the query string,
but if you only supply the start date you'll get back data for just that day.

Expand Down
16 changes: 14 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import nox

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

Expand All @@ -9,7 +10,7 @@ def tests(session):
args = session.posargs
session.install("pipenv")
session.run("pipenv", "sync")
session.run("pytest", *args)
session.run("pipenv", "run", "pytest", *args)


@nox.session
Expand All @@ -22,14 +23,25 @@ def lint(session):


@nox.session
def format(session):
black(session)
isort(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)


@nox.session
def docs(session):
session.chdir("docs")
session.install("-r", "requirements.txt")
session.run("make", "html", external=True)
3 changes: 2 additions & 1 deletion oura/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
It's a description for __init__.py, innit.

"""
from .client import OuraClient, OuraOAuth2Client
from .auth import OAuthRequestHandler, OuraOAuth2Client, PersonalRequestHandler
from .client import OuraClient
from .client_pandas import OuraClientDataFrame
112 changes: 112 additions & 0 deletions oura/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import requests
from requests_oauthlib import OAuth2Session


class OuraOAuth2Client:
"""
Use this for authorizing user and obtaining initial access and refresh token.
Should be one time usage per user.
"""

AUTHORIZE_BASE_URL = "https://cloud.ouraring.com/oauth/authorize"
TOKEN_BASE_URL = "https://api.ouraring.com/oauth/token"
SCOPE = ["email", "personal", "daily"]

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.
:type client_secret: str
"""
self.client_id = client_id
self.client_secret = client_secret

self.session = OAuth2Session(
client_id,
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.

:param scope: Scopes to request from the user. Defaults to self.SCOPE
:type scope: str
:param redirect_uri: Where to redirect after user grants access.
:type redirect_uri: str
"""
self.session.scope = scope or self.SCOPE
if redirect_uri:
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.

: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
)


class OAuthRequestHandler:
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,
):

self.client_id = client_id
self.client_secret = client_secret

token = {}
if access_token:
token["access_token"] = access_token
if refresh_token:
token["refresh_token"] = refresh_token

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

def make_request(self, url):
method = "GET"
response = self._session.request(method, url)
if response.status_code == 401:
self._refresh_token()
response = self._session.request(method, url)
return response

def _refresh_token(self):
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


class PersonalRequestHandler:
def __init__(self, personal_access_token):
self.personal_access_token = personal_access_token

def make_request(self, url):
return requests.get(url, params={"access_token": self.personal_access_token})
Loading