oura-py 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oura_py-0.3.0/PKG-INFO +220 -0
- oura_py-0.3.0/README.md +206 -0
- oura_py-0.3.0/pyproject.toml +35 -0
- oura_py-0.3.0/pyproject.toml.orig +34 -0
- oura_py-0.3.0/src/oura_py/__init__.py +4 -0
- oura_py-0.3.0/src/oura_py/auth/__init__.py +6 -0
- oura_py-0.3.0/src/oura_py/auth/oauth_manager.py +50 -0
- oura_py-0.3.0/src/oura_py/auth/types.py +16 -0
- oura_py-0.3.0/src/oura_py/client/__init__.py +5 -0
- oura_py-0.3.0/src/oura_py/client/oura_client.py +326 -0
- oura_py-0.3.0/src/oura_py/client/request_manager.py +136 -0
- oura_py-0.3.0/src/oura_py/constants.py +48 -0
- oura_py-0.3.0/src/oura_py/data/__init__.py +7 -0
- oura_py-0.3.0/src/oura_py/data/exceptions.py +2 -0
- oura_py-0.3.0/src/oura_py/data/models.py +358 -0
- oura_py-0.3.0/src/oura_py/data/response.py +55 -0
- oura_py-0.3.0/src/oura_py/py.typed +0 -0
oura_py-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oura-py
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A python wrapper for Oura Ring's V2 API.
|
|
5
|
+
Author: Collin Smith
|
|
6
|
+
Author-email: Collin Smith <collinmsmith22@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: requests-oauthlib>=2.0.0
|
|
9
|
+
Requires-Dist: requests>=2.32.3
|
|
10
|
+
Requires-Dist: pydantic>=2.10
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Project-URL: GitHub, https://github.com/col-ms/oura-py
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Oura-Py
|
|
16
|
+
|
|
17
|
+
`oura-py`: A python wrapper for interacting with Oura Ring's V2 API.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
Proper installation documentation to be added upon first production release.
|
|
22
|
+
|
|
23
|
+
## Authentication
|
|
24
|
+
|
|
25
|
+
Oura no longer accepts Personal Access Tokens. This package uses OAuth2
|
|
26
|
+
authorization-code authentication.
|
|
27
|
+
|
|
28
|
+
### One-time Oura setup
|
|
29
|
+
|
|
30
|
+
Before using the client, end users need an Oura developer application:
|
|
31
|
+
|
|
32
|
+
1. Create an OAuth application in the Oura developer portal.
|
|
33
|
+
2. Copy its client ID and client secret.
|
|
34
|
+
3. Add the callback URL used by your application.
|
|
35
|
+
4. Grant the scopes required by the application. The client requests Oura's
|
|
36
|
+
standard data scopes by default; custom scopes can be supplied through the
|
|
37
|
+
lower-level `OuraOAuth2Client` API.
|
|
38
|
+
|
|
39
|
+
The callback URL must match the URL registered with Oura. Authorization and
|
|
40
|
+
token persistence are application responsibilities; use
|
|
41
|
+
`OuraOAuth2Client` to perform the OAuth protocol steps.
|
|
42
|
+
|
|
43
|
+
### Local setup
|
|
44
|
+
|
|
45
|
+
Install the package and put only the application credentials in `.env` (or
|
|
46
|
+
export them in the shell):
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
CLIENT_ID=your-oura-client-id
|
|
50
|
+
CLIENT_SECRET=your-oura-client-secret
|
|
51
|
+
OURA_TOKEN='{"access_token":"...","refresh_token":"..."}'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Using an application-managed token
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import json
|
|
58
|
+
import os
|
|
59
|
+
|
|
60
|
+
from oura_py import OuraClient
|
|
61
|
+
|
|
62
|
+
client = OuraClient(
|
|
63
|
+
client_id=os.environ["CLIENT_ID"],
|
|
64
|
+
client_secret=os.environ["CLIENT_SECRET"],
|
|
65
|
+
token=json.loads(os.environ["OURA_TOKEN"]),
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The application obtains and stores the token. `OuraClient` refreshes it when
|
|
70
|
+
needed and can notify the application when the token changes:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
client = OuraClient(
|
|
74
|
+
client_id=client_id,
|
|
75
|
+
client_secret=client_secret,
|
|
76
|
+
token=stored_token,
|
|
77
|
+
token_updater=save_token,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Client architecture
|
|
82
|
+
|
|
83
|
+
`OuraClient` exposes one method for each supported Oura API resource. Resource
|
|
84
|
+
methods return an `OuraResponse` rather than a bare dictionary or a Pydantic
|
|
85
|
+
model. This keeps the wire response available while allowing callers to opt in
|
|
86
|
+
to typed models when they need them.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import os
|
|
90
|
+
|
|
91
|
+
from oura_py import OuraClient
|
|
92
|
+
|
|
93
|
+
client = OuraClient(
|
|
94
|
+
client_id=os.environ["CLIENT_ID"],
|
|
95
|
+
token=json.loads(os.environ["OURA_TOKEN"]),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
response = client.daily_sleep(
|
|
99
|
+
start_date="2025-01-01",
|
|
100
|
+
end_date="2025-01-07",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
raw_records = response.raw()
|
|
104
|
+
sleep_records = response.model()
|
|
105
|
+
metadata = response.metadata
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`raw()` returns the JSON-compatible payload returned by the client. Collection
|
|
109
|
+
endpoints return a list of records; direct-object endpoints such as
|
|
110
|
+
`personal_info()` return one dictionary. `model()` validates that payload with
|
|
111
|
+
the endpoint's Pydantic model. It returns a list of model instances for a
|
|
112
|
+
collection and one model instance for a direct object. Model conversion is
|
|
113
|
+
cached on the response, so repeated calls do not revalidate the same payload.
|
|
114
|
+
|
|
115
|
+
`metadata` contains request information such as the endpoint and query
|
|
116
|
+
parameters used. It is useful for logging, auditing, and reproducing a
|
|
117
|
+
request.
|
|
118
|
+
|
|
119
|
+
### Collections, pagination, and document IDs
|
|
120
|
+
|
|
121
|
+
Collection methods follow Oura's `data`/`next_token` pagination envelope
|
|
122
|
+
automatically. The client requests subsequent pages and combines their records
|
|
123
|
+
into one response:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
response = client.workout(start_date="2025-01-01", end_date="2025-01-31")
|
|
127
|
+
workouts = response.model()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
To retrieve one collection record, pass its `document_id`. The client sends the
|
|
131
|
+
identifier as a path component (`.../<document_id>`) and returns the direct
|
|
132
|
+
object:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
response = client.daily_sleep(document_id="sleep-record-id")
|
|
136
|
+
sleep = response.model()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`document_id` cannot be combined with `next_token`. Date parameters are not
|
|
140
|
+
sent for a document-specific request.
|
|
141
|
+
|
|
142
|
+
Endpoints backed by timestamps, such as `heartrate()` and
|
|
143
|
+
`ring_battery_level()`, use `start_datetime` and `end_datetime`. If neither is
|
|
144
|
+
provided, the client requests the preceding 24-hour window in UTC. Date-based
|
|
145
|
+
collection endpoints default to the preceding UTC day when dates are omitted.
|
|
146
|
+
|
|
147
|
+
### Webhook subscriptions
|
|
148
|
+
|
|
149
|
+
Webhook subscription methods also return `OuraResponse` objects:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from oura_py.constants import WebhookDataType
|
|
153
|
+
|
|
154
|
+
subscriptions = client.list_webhook_subscriptions()
|
|
155
|
+
for subscription in subscriptions.raw():
|
|
156
|
+
print(subscription["id"])
|
|
157
|
+
|
|
158
|
+
created = client.create_webhook_subscription(
|
|
159
|
+
{
|
|
160
|
+
"callback_url": "https://example.test/oura-webhook",
|
|
161
|
+
"verification_token": "your-verification-token",
|
|
162
|
+
"event_type": "update",
|
|
163
|
+
"data_type": WebhookDataType.SESSION,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
print(created.model())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The list method returns a collection response. Get, create, update, and renew
|
|
170
|
+
methods return a single `WebhookSubscription` object. Deletion returns `None`
|
|
171
|
+
after a successful API request. Webhook management requires the client secret;
|
|
172
|
+
the client sends it using the headers required by Oura's webhook API.
|
|
173
|
+
|
|
174
|
+
### Custom scopes
|
|
175
|
+
|
|
176
|
+
For applications that need a scope set different from the default, use
|
|
177
|
+
`OuraOAuth2Client` directly. The complete flow is shown in
|
|
178
|
+
`examples/custom_scopes.py`; the essential calls are:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from oura_py.auth.oauth_manager import OuraOAuth2Client
|
|
182
|
+
|
|
183
|
+
oauth_client = OuraOAuth2Client(client_id, client_secret)
|
|
184
|
+
authorization_url, state = oauth_client.get_authorization_url(
|
|
185
|
+
scope=["personal", "daily", "heartrate"],
|
|
186
|
+
redirect_uri="http://localhost:8080/callback",
|
|
187
|
+
)
|
|
188
|
+
# Send the user to authorization_url and validate the returned state.
|
|
189
|
+
token = oauth_client.exchange_code(authorization_code)
|
|
190
|
+
client = OuraClient(
|
|
191
|
+
client_id=client_id,
|
|
192
|
+
client_secret=client_secret,
|
|
193
|
+
token=token,
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Webhooks
|
|
198
|
+
|
|
199
|
+
See `examples/webhook_example.py` for a complete subscription and receiver
|
|
200
|
+
example. The script uses ngrok's official Python SDK to expose the local receiver and derives the
|
|
201
|
+
public callback URL automatically. An ngrok authtoken may be required; set
|
|
202
|
+
`NGROK_AUTHTOKEN` if your ngrok account requires one.
|
|
203
|
+
|
|
204
|
+
The example expects these environment variables:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
CLIENT_ID=your-oura-client-id
|
|
208
|
+
CLIENT_SECRET=your-oura-client-secret
|
|
209
|
+
OURA_TOKEN='{"access_token":"...","refresh_token":"..."}'
|
|
210
|
+
WEBHOOK_VERIFICATION_TOKEN=choose-a-secret-value
|
|
211
|
+
NGROK_AUTHTOKEN=your-ngrok-authtoken
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The example uses port `8000`, subscribes to `daily_sleep` updates, and uses
|
|
215
|
+
the generated ngrok URL automatically.
|
|
216
|
+
|
|
217
|
+
It handles Oura's verification challenge, validates the
|
|
218
|
+
`x-oura-signature` HMAC, acknowledges the notification quickly, and prints
|
|
219
|
+
the event metadata. Production applications should enqueue the event and
|
|
220
|
+
fetch the changed resource asynchronously using the event's `object_id`.
|
oura_py-0.3.0/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Oura-Py
|
|
2
|
+
|
|
3
|
+
`oura-py`: A python wrapper for interacting with Oura Ring's V2 API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Proper installation documentation to be added upon first production release.
|
|
8
|
+
|
|
9
|
+
## Authentication
|
|
10
|
+
|
|
11
|
+
Oura no longer accepts Personal Access Tokens. This package uses OAuth2
|
|
12
|
+
authorization-code authentication.
|
|
13
|
+
|
|
14
|
+
### One-time Oura setup
|
|
15
|
+
|
|
16
|
+
Before using the client, end users need an Oura developer application:
|
|
17
|
+
|
|
18
|
+
1. Create an OAuth application in the Oura developer portal.
|
|
19
|
+
2. Copy its client ID and client secret.
|
|
20
|
+
3. Add the callback URL used by your application.
|
|
21
|
+
4. Grant the scopes required by the application. The client requests Oura's
|
|
22
|
+
standard data scopes by default; custom scopes can be supplied through the
|
|
23
|
+
lower-level `OuraOAuth2Client` API.
|
|
24
|
+
|
|
25
|
+
The callback URL must match the URL registered with Oura. Authorization and
|
|
26
|
+
token persistence are application responsibilities; use
|
|
27
|
+
`OuraOAuth2Client` to perform the OAuth protocol steps.
|
|
28
|
+
|
|
29
|
+
### Local setup
|
|
30
|
+
|
|
31
|
+
Install the package and put only the application credentials in `.env` (or
|
|
32
|
+
export them in the shell):
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
CLIENT_ID=your-oura-client-id
|
|
36
|
+
CLIENT_SECRET=your-oura-client-secret
|
|
37
|
+
OURA_TOKEN='{"access_token":"...","refresh_token":"..."}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Using an application-managed token
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import json
|
|
44
|
+
import os
|
|
45
|
+
|
|
46
|
+
from oura_py import OuraClient
|
|
47
|
+
|
|
48
|
+
client = OuraClient(
|
|
49
|
+
client_id=os.environ["CLIENT_ID"],
|
|
50
|
+
client_secret=os.environ["CLIENT_SECRET"],
|
|
51
|
+
token=json.loads(os.environ["OURA_TOKEN"]),
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The application obtains and stores the token. `OuraClient` refreshes it when
|
|
56
|
+
needed and can notify the application when the token changes:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = OuraClient(
|
|
60
|
+
client_id=client_id,
|
|
61
|
+
client_secret=client_secret,
|
|
62
|
+
token=stored_token,
|
|
63
|
+
token_updater=save_token,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Client architecture
|
|
68
|
+
|
|
69
|
+
`OuraClient` exposes one method for each supported Oura API resource. Resource
|
|
70
|
+
methods return an `OuraResponse` rather than a bare dictionary or a Pydantic
|
|
71
|
+
model. This keeps the wire response available while allowing callers to opt in
|
|
72
|
+
to typed models when they need them.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import os
|
|
76
|
+
|
|
77
|
+
from oura_py import OuraClient
|
|
78
|
+
|
|
79
|
+
client = OuraClient(
|
|
80
|
+
client_id=os.environ["CLIENT_ID"],
|
|
81
|
+
token=json.loads(os.environ["OURA_TOKEN"]),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
response = client.daily_sleep(
|
|
85
|
+
start_date="2025-01-01",
|
|
86
|
+
end_date="2025-01-07",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
raw_records = response.raw()
|
|
90
|
+
sleep_records = response.model()
|
|
91
|
+
metadata = response.metadata
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`raw()` returns the JSON-compatible payload returned by the client. Collection
|
|
95
|
+
endpoints return a list of records; direct-object endpoints such as
|
|
96
|
+
`personal_info()` return one dictionary. `model()` validates that payload with
|
|
97
|
+
the endpoint's Pydantic model. It returns a list of model instances for a
|
|
98
|
+
collection and one model instance for a direct object. Model conversion is
|
|
99
|
+
cached on the response, so repeated calls do not revalidate the same payload.
|
|
100
|
+
|
|
101
|
+
`metadata` contains request information such as the endpoint and query
|
|
102
|
+
parameters used. It is useful for logging, auditing, and reproducing a
|
|
103
|
+
request.
|
|
104
|
+
|
|
105
|
+
### Collections, pagination, and document IDs
|
|
106
|
+
|
|
107
|
+
Collection methods follow Oura's `data`/`next_token` pagination envelope
|
|
108
|
+
automatically. The client requests subsequent pages and combines their records
|
|
109
|
+
into one response:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
response = client.workout(start_date="2025-01-01", end_date="2025-01-31")
|
|
113
|
+
workouts = response.model()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
To retrieve one collection record, pass its `document_id`. The client sends the
|
|
117
|
+
identifier as a path component (`.../<document_id>`) and returns the direct
|
|
118
|
+
object:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
response = client.daily_sleep(document_id="sleep-record-id")
|
|
122
|
+
sleep = response.model()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`document_id` cannot be combined with `next_token`. Date parameters are not
|
|
126
|
+
sent for a document-specific request.
|
|
127
|
+
|
|
128
|
+
Endpoints backed by timestamps, such as `heartrate()` and
|
|
129
|
+
`ring_battery_level()`, use `start_datetime` and `end_datetime`. If neither is
|
|
130
|
+
provided, the client requests the preceding 24-hour window in UTC. Date-based
|
|
131
|
+
collection endpoints default to the preceding UTC day when dates are omitted.
|
|
132
|
+
|
|
133
|
+
### Webhook subscriptions
|
|
134
|
+
|
|
135
|
+
Webhook subscription methods also return `OuraResponse` objects:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from oura_py.constants import WebhookDataType
|
|
139
|
+
|
|
140
|
+
subscriptions = client.list_webhook_subscriptions()
|
|
141
|
+
for subscription in subscriptions.raw():
|
|
142
|
+
print(subscription["id"])
|
|
143
|
+
|
|
144
|
+
created = client.create_webhook_subscription(
|
|
145
|
+
{
|
|
146
|
+
"callback_url": "https://example.test/oura-webhook",
|
|
147
|
+
"verification_token": "your-verification-token",
|
|
148
|
+
"event_type": "update",
|
|
149
|
+
"data_type": WebhookDataType.SESSION,
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
print(created.model())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The list method returns a collection response. Get, create, update, and renew
|
|
156
|
+
methods return a single `WebhookSubscription` object. Deletion returns `None`
|
|
157
|
+
after a successful API request. Webhook management requires the client secret;
|
|
158
|
+
the client sends it using the headers required by Oura's webhook API.
|
|
159
|
+
|
|
160
|
+
### Custom scopes
|
|
161
|
+
|
|
162
|
+
For applications that need a scope set different from the default, use
|
|
163
|
+
`OuraOAuth2Client` directly. The complete flow is shown in
|
|
164
|
+
`examples/custom_scopes.py`; the essential calls are:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from oura_py.auth.oauth_manager import OuraOAuth2Client
|
|
168
|
+
|
|
169
|
+
oauth_client = OuraOAuth2Client(client_id, client_secret)
|
|
170
|
+
authorization_url, state = oauth_client.get_authorization_url(
|
|
171
|
+
scope=["personal", "daily", "heartrate"],
|
|
172
|
+
redirect_uri="http://localhost:8080/callback",
|
|
173
|
+
)
|
|
174
|
+
# Send the user to authorization_url and validate the returned state.
|
|
175
|
+
token = oauth_client.exchange_code(authorization_code)
|
|
176
|
+
client = OuraClient(
|
|
177
|
+
client_id=client_id,
|
|
178
|
+
client_secret=client_secret,
|
|
179
|
+
token=token,
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Webhooks
|
|
184
|
+
|
|
185
|
+
See `examples/webhook_example.py` for a complete subscription and receiver
|
|
186
|
+
example. The script uses ngrok's official Python SDK to expose the local receiver and derives the
|
|
187
|
+
public callback URL automatically. An ngrok authtoken may be required; set
|
|
188
|
+
`NGROK_AUTHTOKEN` if your ngrok account requires one.
|
|
189
|
+
|
|
190
|
+
The example expects these environment variables:
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
CLIENT_ID=your-oura-client-id
|
|
194
|
+
CLIENT_SECRET=your-oura-client-secret
|
|
195
|
+
OURA_TOKEN='{"access_token":"...","refresh_token":"..."}'
|
|
196
|
+
WEBHOOK_VERIFICATION_TOKEN=choose-a-secret-value
|
|
197
|
+
NGROK_AUTHTOKEN=your-ngrok-authtoken
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The example uses port `8000`, subscribes to `daily_sleep` updates, and uses
|
|
201
|
+
the generated ngrok URL automatically.
|
|
202
|
+
|
|
203
|
+
It handles Oura's verification challenge, validates the
|
|
204
|
+
`x-oura-signature` HMAC, acknowledges the notification quickly, and prints
|
|
205
|
+
the event metadata. Production applications should enqueue the event and
|
|
206
|
+
fetch the changed resource asynchronously using the event's `object_id`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oura-py"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "A python wrapper for Oura Ring's V2 API."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"requests-oauthlib>=2.0.0",
|
|
10
|
+
"requests>=2.32.3",
|
|
11
|
+
"pydantic>=2.10",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "Collin Smith"
|
|
16
|
+
email = "collinmsmith22@gmail.com"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
GitHub = "https://github.com/col-ms/oura-py"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.8.14,<0.13.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pre-commit>=4.1.0",
|
|
28
|
+
"pylint>=3.3.4",
|
|
29
|
+
"pytest-cov>=6.0.0",
|
|
30
|
+
"pytest>=8.3.4",
|
|
31
|
+
"ngrok>=1.7.0",
|
|
32
|
+
"python-dotenv>=1.0.1",
|
|
33
|
+
"ruff>=0.9.6",
|
|
34
|
+
"ty>=0.0.75",
|
|
35
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oura-py"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "A python wrapper for Oura Ring's V2 API."
|
|
5
|
+
authors = [{ name = "Collin Smith", email = "collinmsmith22@gmail.com" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"requests-oauthlib>=2.0.0",
|
|
11
|
+
"requests>=2.32.3",
|
|
12
|
+
"pydantic>=2.10",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
GitHub = "https://github.com/col-ms/oura-py"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.8.14,<0.13.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"pre-commit>=4.1.0",
|
|
27
|
+
"pylint>=3.3.4",
|
|
28
|
+
"pytest-cov>=6.0.0",
|
|
29
|
+
"pytest>=8.3.4",
|
|
30
|
+
"ngrok>=1.7.0",
|
|
31
|
+
"python-dotenv>=1.0.1",
|
|
32
|
+
"ruff>=0.9.6",
|
|
33
|
+
"ty>=0.0.75",
|
|
34
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from requests_oauthlib import OAuth2Session
|
|
2
|
+
|
|
3
|
+
from oura_py.auth.types import OAuthToken
|
|
4
|
+
from oura_py.constants import AUTHORIZE_URL, TOKEN_URL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OuraOAuth2Client:
|
|
8
|
+
def __init__(self, client_id: str, client_secret: str) -> None:
|
|
9
|
+
if not client_id or not client_secret:
|
|
10
|
+
raise ValueError("client_id and client_secret are required")
|
|
11
|
+
self.client_id = client_id
|
|
12
|
+
self.client_secret = client_secret
|
|
13
|
+
self.session = OAuth2Session(
|
|
14
|
+
client_id=self.client_id,
|
|
15
|
+
auto_refresh_url=TOKEN_URL,
|
|
16
|
+
auto_refresh_kwargs={
|
|
17
|
+
"client_id": self.client_id,
|
|
18
|
+
"client_secret": self.client_secret,
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def get_authorization_url(
|
|
23
|
+
self,
|
|
24
|
+
scope: list[str] | tuple[str] | None = None,
|
|
25
|
+
redirect_uri: str | None = None,
|
|
26
|
+
state: str | None = None,
|
|
27
|
+
) -> tuple[str, str]:
|
|
28
|
+
self.session.scope = scope
|
|
29
|
+
self.session.redirect_uri = redirect_uri
|
|
30
|
+
return self.session.authorization_url(url=AUTHORIZE_URL, state=state)
|
|
31
|
+
|
|
32
|
+
def exchange_code(self, code: str) -> OAuthToken:
|
|
33
|
+
return self.session.fetch_token(
|
|
34
|
+
token_url=TOKEN_URL,
|
|
35
|
+
code=code,
|
|
36
|
+
client_secret=self.client_secret,
|
|
37
|
+
include_client_id=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def refresh_access_token(self, refresh_token: str) -> OAuthToken:
|
|
41
|
+
if not refresh_token:
|
|
42
|
+
raise ValueError("refresh_token is required")
|
|
43
|
+
token = self.session.refresh_token(
|
|
44
|
+
token_url=TOKEN_URL,
|
|
45
|
+
refresh_token=refresh_token,
|
|
46
|
+
client_id=self.client_id,
|
|
47
|
+
client_secret=self.client_secret,
|
|
48
|
+
)
|
|
49
|
+
token.setdefault("refresh_token", refresh_token)
|
|
50
|
+
return token
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Required, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OAuthToken(TypedDict, total=False):
|
|
6
|
+
"""OAuth credentials accepted by the Oura client."""
|
|
7
|
+
|
|
8
|
+
access_token: Required[str]
|
|
9
|
+
token_type: str
|
|
10
|
+
refresh_token: str
|
|
11
|
+
expires_at: float
|
|
12
|
+
expires_in: int
|
|
13
|
+
scope: str | list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
TokenUpdater = Callable[[OAuthToken], None]
|