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 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`.
@@ -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,4 @@
1
+ from oura_py.client.oura_client import OuraClient
2
+ from oura_py.constants import WebhookDataType
3
+
4
+ __all__ = ["OuraClient", "WebhookDataType"]
@@ -0,0 +1,6 @@
1
+ """OAuth2 protocol helpers for applications using :mod:`oura_py`."""
2
+
3
+ from oura_py.auth.oauth_manager import OuraOAuth2Client
4
+ from oura_py.auth.types import OAuthToken, TokenUpdater
5
+
6
+ __all__ = ["OAuthToken", "OuraOAuth2Client", "TokenUpdater"]
@@ -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]
@@ -0,0 +1,5 @@
1
+ """Authenticated API clients and HTTP request helpers."""
2
+
3
+ from oura_py.client.oura_client import OuraClient
4
+
5
+ __all__ = ["OuraClient"]