hubble-sdk-python 0.0.3__tar.gz → 0.1.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.
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/CHANGELOG.md +13 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/PKG-INFO +4 -3
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/README.md +3 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/attach_client_authorization.py +19 -16
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/client.py +9 -11
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/__init__.py +0 -8
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent.py +0 -25
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_link_request.py +12 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_source_status.py +2 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_source_type.py +2 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_status.py +2 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/device_authorization_request.py +21 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/o_auth_error_error.py +2 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/operation_outcome.py +3 -1
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/patient_address.py +9 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/token_request_grant_type.py +2 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_hubble.py +66 -2
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/pyproject.toml +1 -1
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/tests/test_hubble.py +100 -2
- hubble_sdk_python-0.0.3/hubble_sdk/_generated/models/attach_client_authorization_response_201.py +0 -100
- hubble_sdk_python-0.0.3/hubble_sdk/_generated/models/attach_client_authorization_response_201_status.py +0 -8
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/.gitignore +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/LICENSE +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/config.yaml +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/__init__.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/__init__.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/__init__.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/__init__.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/create_consent_link.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/exchange_token.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/export_patient_pdf.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/get_consent.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/list_consents.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/read_fhir.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/revoke_consent.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/errors.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/api_error.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/attach_client_authorization_body.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_error.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_list.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_source.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/device_authorization_response.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/fhir_resource.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/o_auth_error.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/operation_outcome_issue_item.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/patient_demographics.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/provider_seed.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/token_request.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/token_response.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/types.py +0 -0
- {hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/pydoc-markdown.yml +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0](https://github.com/hubble-dot-ai/hubble-sdks/compare/hubble-sdk-python-v0.0.3...hubble-sdk-python-v0.1.0) (2026-09-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* `Consent.granted_at` is removed. Callers reading it must derive authorization time another way (e.g. status transitions).
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add attach_authorization for BYO HIPAA authorization ([#41](https://github.com/hubble-dot-ai/hubble-sdks/issues/41)) ([66baf11](https://github.com/hubble-dot-ai/hubble-sdks/commit/66baf1192291b31cb03e325be44f1e6c5e4371f3))
|
|
13
|
+
* drop granted_at from Consent ([#44](https://github.com/hubble-dot-ai/hubble-sdks/issues/44)) ([cb61be8](https://github.com/hubble-dot-ai/hubble-sdks/commit/cb61be8dc6e8c4784e30332cb4e31317f773ec3b))
|
|
14
|
+
* support represented organizations in SDK links ([#45](https://github.com/hubble-dot-ai/hubble-sdks/issues/45)) ([430a1d0](https://github.com/hubble-dot-ai/hubble-sdks/commit/430a1d022f75ea645d27b0b7ebf87b910f1c51a9))
|
|
15
|
+
|
|
3
16
|
## [0.0.3](https://github.com/hubble-dot-ai/hubble-sdks/compare/hubble-sdk-python-v0.0.2...hubble-sdk-python-v0.0.3) (2026-08-23)
|
|
4
17
|
|
|
5
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hubble-sdk-python
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Python client for the Hubble Patient Access API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/hubble-dot-ai/hubble-sdks
|
|
6
6
|
Project-URL: Repository, https://github.com/hubble-dot-ai/hubble-sdks
|
|
@@ -45,8 +45,9 @@ from hubble_sdk import Hubble
|
|
|
45
45
|
hub = Hubble(client_id="hbl_client_prod_...", client_secret="hbl_prod_...")
|
|
46
46
|
|
|
47
47
|
# 1. Create a patient link and send the patient to it. Optionally prefill with
|
|
48
|
-
# patient={"first_name": ...} demographics
|
|
49
|
-
|
|
48
|
+
# patient={"first_name": ...} demographics, providers=[{"name": ...}], and
|
|
49
|
+
# the optional organization your verified Hubble organization represents.
|
|
50
|
+
link = hub.create_link(patient_ref="abc123", on_behalf_of_organization_name="Acme Health")
|
|
50
51
|
print(link.verification_uri_complete)
|
|
51
52
|
|
|
52
53
|
# 2. Wait for the patient to authorize (polls the token endpoint for you).
|
|
@@ -13,8 +13,9 @@ from hubble_sdk import Hubble
|
|
|
13
13
|
hub = Hubble(client_id="hbl_client_prod_...", client_secret="hbl_prod_...")
|
|
14
14
|
|
|
15
15
|
# 1. Create a patient link and send the patient to it. Optionally prefill with
|
|
16
|
-
# patient={"first_name": ...} demographics
|
|
17
|
-
|
|
16
|
+
# patient={"first_name": ...} demographics, providers=[{"name": ...}], and
|
|
17
|
+
# the optional organization your verified Hubble organization represents.
|
|
18
|
+
link = hub.create_link(patient_ref="abc123", on_behalf_of_organization_name="Acme Health")
|
|
18
19
|
print(link.verification_uri_complete)
|
|
19
20
|
|
|
20
21
|
# 2. Wait for the patient to authorize (polls the token endpoint for you).
|
|
@@ -8,9 +8,7 @@ from ... import errors
|
|
|
8
8
|
from ...client import AuthenticatedClient, Client
|
|
9
9
|
from ...models.api_error import ApiError
|
|
10
10
|
from ...models.attach_client_authorization_body import AttachClientAuthorizationBody
|
|
11
|
-
from ...models.
|
|
12
|
-
AttachClientAuthorizationResponse201,
|
|
13
|
-
)
|
|
11
|
+
from ...models.consent import Consent
|
|
14
12
|
from ...types import Response
|
|
15
13
|
|
|
16
14
|
|
|
@@ -38,11 +36,11 @@ def _get_kwargs(
|
|
|
38
36
|
|
|
39
37
|
def _parse_response(
|
|
40
38
|
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
41
|
-
) -> ApiError |
|
|
42
|
-
if response.status_code ==
|
|
43
|
-
|
|
39
|
+
) -> ApiError | Consent | None:
|
|
40
|
+
if response.status_code == 200:
|
|
41
|
+
response_200 = Consent.from_dict(response.json())
|
|
44
42
|
|
|
45
|
-
return
|
|
43
|
+
return response_200
|
|
46
44
|
|
|
47
45
|
if response.status_code == 400:
|
|
48
46
|
response_400 = ApiError.from_dict(response.json())
|
|
@@ -64,6 +62,11 @@ def _parse_response(
|
|
|
64
62
|
|
|
65
63
|
return response_404
|
|
66
64
|
|
|
65
|
+
if response.status_code == 409:
|
|
66
|
+
response_409 = ApiError.from_dict(response.json())
|
|
67
|
+
|
|
68
|
+
return response_409
|
|
69
|
+
|
|
67
70
|
if response.status_code == 413:
|
|
68
71
|
response_413 = ApiError.from_dict(response.json())
|
|
69
72
|
|
|
@@ -77,7 +80,7 @@ def _parse_response(
|
|
|
77
80
|
|
|
78
81
|
def _build_response(
|
|
79
82
|
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
80
|
-
) -> Response[ApiError |
|
|
83
|
+
) -> Response[ApiError | Consent]:
|
|
81
84
|
return Response(
|
|
82
85
|
status_code=HTTPStatus(response.status_code),
|
|
83
86
|
content=response.content,
|
|
@@ -91,7 +94,7 @@ def sync_detailed(
|
|
|
91
94
|
*,
|
|
92
95
|
client: AuthenticatedClient,
|
|
93
96
|
body: AttachClientAuthorizationBody,
|
|
94
|
-
) -> Response[ApiError |
|
|
97
|
+
) -> Response[ApiError | Consent]:
|
|
95
98
|
"""Attach a customer-supplied HIPAA authorization
|
|
96
99
|
|
|
97
100
|
Upload an already-signed HIPAA authorization for a consent and grant delegated retrieval from the
|
|
@@ -107,7 +110,7 @@ def sync_detailed(
|
|
|
107
110
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
108
111
|
|
|
109
112
|
Returns:
|
|
110
|
-
Response[ApiError |
|
|
113
|
+
Response[ApiError | Consent]
|
|
111
114
|
"""
|
|
112
115
|
|
|
113
116
|
kwargs = _get_kwargs(
|
|
@@ -127,7 +130,7 @@ def sync(
|
|
|
127
130
|
*,
|
|
128
131
|
client: AuthenticatedClient,
|
|
129
132
|
body: AttachClientAuthorizationBody,
|
|
130
|
-
) -> ApiError |
|
|
133
|
+
) -> ApiError | Consent | None:
|
|
131
134
|
"""Attach a customer-supplied HIPAA authorization
|
|
132
135
|
|
|
133
136
|
Upload an already-signed HIPAA authorization for a consent and grant delegated retrieval from the
|
|
@@ -143,7 +146,7 @@ def sync(
|
|
|
143
146
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
144
147
|
|
|
145
148
|
Returns:
|
|
146
|
-
ApiError |
|
|
149
|
+
ApiError | Consent
|
|
147
150
|
"""
|
|
148
151
|
|
|
149
152
|
return sync_detailed(
|
|
@@ -158,7 +161,7 @@ async def asyncio_detailed(
|
|
|
158
161
|
*,
|
|
159
162
|
client: AuthenticatedClient,
|
|
160
163
|
body: AttachClientAuthorizationBody,
|
|
161
|
-
) -> Response[ApiError |
|
|
164
|
+
) -> Response[ApiError | Consent]:
|
|
162
165
|
"""Attach a customer-supplied HIPAA authorization
|
|
163
166
|
|
|
164
167
|
Upload an already-signed HIPAA authorization for a consent and grant delegated retrieval from the
|
|
@@ -174,7 +177,7 @@ async def asyncio_detailed(
|
|
|
174
177
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
175
178
|
|
|
176
179
|
Returns:
|
|
177
|
-
Response[ApiError |
|
|
180
|
+
Response[ApiError | Consent]
|
|
178
181
|
"""
|
|
179
182
|
|
|
180
183
|
kwargs = _get_kwargs(
|
|
@@ -192,7 +195,7 @@ async def asyncio(
|
|
|
192
195
|
*,
|
|
193
196
|
client: AuthenticatedClient,
|
|
194
197
|
body: AttachClientAuthorizationBody,
|
|
195
|
-
) -> ApiError |
|
|
198
|
+
) -> ApiError | Consent | None:
|
|
196
199
|
"""Attach a customer-supplied HIPAA authorization
|
|
197
200
|
|
|
198
201
|
Upload an already-signed HIPAA authorization for a consent and grant delegated retrieval from the
|
|
@@ -208,7 +211,7 @@ async def asyncio(
|
|
|
208
211
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
209
212
|
|
|
210
213
|
Returns:
|
|
211
|
-
ApiError |
|
|
214
|
+
ApiError | Consent
|
|
212
215
|
"""
|
|
213
216
|
|
|
214
217
|
return (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ssl
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, Self
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
from attrs import define, evolve, field
|
|
@@ -73,7 +73,7 @@ class Client:
|
|
|
73
73
|
self._async_client.timeout = timeout
|
|
74
74
|
return evolve(self, timeout=timeout)
|
|
75
75
|
|
|
76
|
-
def set_httpx_client(self, client: httpx.Client) ->
|
|
76
|
+
def set_httpx_client(self, client: httpx.Client) -> Self:
|
|
77
77
|
"""Manually set the underlying httpx.Client
|
|
78
78
|
|
|
79
79
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -95,7 +95,7 @@ class Client:
|
|
|
95
95
|
)
|
|
96
96
|
return self._client
|
|
97
97
|
|
|
98
|
-
def __enter__(self) ->
|
|
98
|
+
def __enter__(self) -> Self:
|
|
99
99
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
|
100
100
|
self.get_httpx_client().__enter__()
|
|
101
101
|
return self
|
|
@@ -104,7 +104,7 @@ class Client:
|
|
|
104
104
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
|
105
105
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
|
106
106
|
|
|
107
|
-
def set_async_httpx_client(self, async_client: httpx.AsyncClient) ->
|
|
107
|
+
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
|
|
108
108
|
"""Manually set the underlying httpx.AsyncClient
|
|
109
109
|
|
|
110
110
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -126,7 +126,7 @@ class Client:
|
|
|
126
126
|
)
|
|
127
127
|
return self._async_client
|
|
128
128
|
|
|
129
|
-
async def __aenter__(self) ->
|
|
129
|
+
async def __aenter__(self) -> Self:
|
|
130
130
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
|
131
131
|
await self.get_async_httpx_client().__aenter__()
|
|
132
132
|
return self
|
|
@@ -211,7 +211,7 @@ class AuthenticatedClient:
|
|
|
211
211
|
self._async_client.timeout = timeout
|
|
212
212
|
return evolve(self, timeout=timeout)
|
|
213
213
|
|
|
214
|
-
def set_httpx_client(self, client: httpx.Client) ->
|
|
214
|
+
def set_httpx_client(self, client: httpx.Client) -> Self:
|
|
215
215
|
"""Manually set the underlying httpx.Client
|
|
216
216
|
|
|
217
217
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -236,7 +236,7 @@ class AuthenticatedClient:
|
|
|
236
236
|
)
|
|
237
237
|
return self._client
|
|
238
238
|
|
|
239
|
-
def __enter__(self) ->
|
|
239
|
+
def __enter__(self) -> Self:
|
|
240
240
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
|
241
241
|
self.get_httpx_client().__enter__()
|
|
242
242
|
return self
|
|
@@ -245,9 +245,7 @@ class AuthenticatedClient:
|
|
|
245
245
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
|
246
246
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
|
247
247
|
|
|
248
|
-
def set_async_httpx_client(
|
|
249
|
-
self, async_client: httpx.AsyncClient
|
|
250
|
-
) -> "AuthenticatedClient":
|
|
248
|
+
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
|
|
251
249
|
"""Manually set the underlying httpx.AsyncClient
|
|
252
250
|
|
|
253
251
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -272,7 +270,7 @@ class AuthenticatedClient:
|
|
|
272
270
|
)
|
|
273
271
|
return self._async_client
|
|
274
272
|
|
|
275
|
-
async def __aenter__(self) ->
|
|
273
|
+
async def __aenter__(self) -> Self:
|
|
276
274
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
|
277
275
|
await self.get_async_httpx_client().__aenter__()
|
|
278
276
|
return self
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/__init__.py
RENAMED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from .api_error import ApiError
|
|
4
4
|
from .attach_client_authorization_body import AttachClientAuthorizationBody
|
|
5
|
-
from .attach_client_authorization_response_201 import (
|
|
6
|
-
AttachClientAuthorizationResponse201,
|
|
7
|
-
)
|
|
8
|
-
from .attach_client_authorization_response_201_status import (
|
|
9
|
-
AttachClientAuthorizationResponse201Status,
|
|
10
|
-
)
|
|
11
5
|
from .consent import Consent
|
|
12
6
|
from .consent_error import ConsentError
|
|
13
7
|
from .consent_link_request import ConsentLinkRequest
|
|
@@ -33,8 +27,6 @@ from .token_response import TokenResponse
|
|
|
33
27
|
__all__ = (
|
|
34
28
|
"ApiError",
|
|
35
29
|
"AttachClientAuthorizationBody",
|
|
36
|
-
"AttachClientAuthorizationResponse201",
|
|
37
|
-
"AttachClientAuthorizationResponse201Status",
|
|
38
30
|
"Consent",
|
|
39
31
|
"ConsentError",
|
|
40
32
|
"ConsentLinkRequest",
|
|
@@ -24,7 +24,6 @@ class Consent:
|
|
|
24
24
|
status (ConsentStatus):
|
|
25
25
|
patient_ref (None | str): Your own reference for the patient, echoed back from consent creation.
|
|
26
26
|
created_at (datetime.datetime): When the consent was created.
|
|
27
|
-
granted_at (datetime.datetime | None): When the patient authorized. Null until active.
|
|
28
27
|
expires_at (datetime.datetime): When a pending link expires, or when the grant lapses.
|
|
29
28
|
sources (list[ConsentSource]): Each source on the consent and its progress (live vs one-time,
|
|
30
29
|
available/pending/failed).
|
|
@@ -35,7 +34,6 @@ class Consent:
|
|
|
35
34
|
status: ConsentStatus
|
|
36
35
|
patient_ref: None | str
|
|
37
36
|
created_at: datetime.datetime
|
|
38
|
-
granted_at: datetime.datetime | None
|
|
39
37
|
expires_at: datetime.datetime
|
|
40
38
|
sources: list[ConsentSource]
|
|
41
39
|
error: ConsentError | None
|
|
@@ -52,12 +50,6 @@ class Consent:
|
|
|
52
50
|
|
|
53
51
|
created_at = self.created_at.isoformat()
|
|
54
52
|
|
|
55
|
-
granted_at: None | str
|
|
56
|
-
if isinstance(self.granted_at, datetime.datetime):
|
|
57
|
-
granted_at = self.granted_at.isoformat()
|
|
58
|
-
else:
|
|
59
|
-
granted_at = self.granted_at
|
|
60
|
-
|
|
61
53
|
expires_at = self.expires_at.isoformat()
|
|
62
54
|
|
|
63
55
|
sources = []
|
|
@@ -79,7 +71,6 @@ class Consent:
|
|
|
79
71
|
"status": status,
|
|
80
72
|
"patient_ref": patient_ref,
|
|
81
73
|
"created_at": created_at,
|
|
82
|
-
"granted_at": granted_at,
|
|
83
74
|
"expires_at": expires_at,
|
|
84
75
|
"sources": sources,
|
|
85
76
|
"error": error,
|
|
@@ -107,21 +98,6 @@ class Consent:
|
|
|
107
98
|
|
|
108
99
|
created_at = datetime.datetime.fromisoformat(d.pop("created_at"))
|
|
109
100
|
|
|
110
|
-
def _parse_granted_at(data: object) -> datetime.datetime | None:
|
|
111
|
-
if data is None:
|
|
112
|
-
return data
|
|
113
|
-
try:
|
|
114
|
-
if not isinstance(data, str):
|
|
115
|
-
raise TypeError()
|
|
116
|
-
granted_at_type_0 = datetime.datetime.fromisoformat(data)
|
|
117
|
-
|
|
118
|
-
return granted_at_type_0
|
|
119
|
-
except (TypeError, ValueError, AttributeError, KeyError):
|
|
120
|
-
pass
|
|
121
|
-
return cast(datetime.datetime | None, data)
|
|
122
|
-
|
|
123
|
-
granted_at = _parse_granted_at(d.pop("granted_at"))
|
|
124
|
-
|
|
125
101
|
expires_at = datetime.datetime.fromisoformat(d.pop("expires_at"))
|
|
126
102
|
|
|
127
103
|
sources = []
|
|
@@ -151,7 +127,6 @@ class Consent:
|
|
|
151
127
|
status=status,
|
|
152
128
|
patient_ref=patient_ref,
|
|
153
129
|
created_at=created_at,
|
|
154
|
-
granted_at=granted_at,
|
|
155
130
|
expires_at=expires_at,
|
|
156
131
|
sources=sources,
|
|
157
132
|
error=error,
|
|
@@ -23,11 +23,14 @@ class ConsentLinkRequest:
|
|
|
23
23
|
patient (PatientDemographics | Unset):
|
|
24
24
|
providers (list[ProviderSeed] | Unset): Provider seeds to prefill the patient's list. Seeded by name (freeform);
|
|
25
25
|
the patient can add, change, or remove them on the link page.
|
|
26
|
+
on_behalf_of_organization_name (str | Unset): Organization the verified requester is acting on behalf of for
|
|
27
|
+
this link.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
patient_ref: str | Unset = UNSET
|
|
29
31
|
patient: PatientDemographics | Unset = UNSET
|
|
30
32
|
providers: list[ProviderSeed] | Unset = UNSET
|
|
33
|
+
on_behalf_of_organization_name: str | Unset = UNSET
|
|
31
34
|
|
|
32
35
|
def to_dict(self) -> dict[str, Any]:
|
|
33
36
|
patient_ref = self.patient_ref
|
|
@@ -43,6 +46,8 @@ class ConsentLinkRequest:
|
|
|
43
46
|
providers_item = providers_item_data.to_dict()
|
|
44
47
|
providers.append(providers_item)
|
|
45
48
|
|
|
49
|
+
on_behalf_of_organization_name = self.on_behalf_of_organization_name
|
|
50
|
+
|
|
46
51
|
field_dict: dict[str, Any] = {}
|
|
47
52
|
|
|
48
53
|
field_dict.update({})
|
|
@@ -52,6 +57,10 @@ class ConsentLinkRequest:
|
|
|
52
57
|
field_dict["patient"] = patient
|
|
53
58
|
if providers is not UNSET:
|
|
54
59
|
field_dict["providers"] = providers
|
|
60
|
+
if on_behalf_of_organization_name is not UNSET:
|
|
61
|
+
field_dict["on_behalf_of_organization_name"] = (
|
|
62
|
+
on_behalf_of_organization_name
|
|
63
|
+
)
|
|
55
64
|
|
|
56
65
|
return field_dict
|
|
57
66
|
|
|
@@ -79,10 +88,13 @@ class ConsentLinkRequest:
|
|
|
79
88
|
|
|
80
89
|
providers.append(providers_item)
|
|
81
90
|
|
|
91
|
+
on_behalf_of_organization_name = d.pop("on_behalf_of_organization_name", UNSET)
|
|
92
|
+
|
|
82
93
|
consent_link_request = cls(
|
|
83
94
|
patient_ref=patient_ref,
|
|
84
95
|
patient=patient,
|
|
85
96
|
providers=providers,
|
|
97
|
+
on_behalf_of_organization_name=on_behalf_of_organization_name,
|
|
86
98
|
)
|
|
87
99
|
|
|
88
100
|
return consent_link_request
|
|
@@ -20,6 +20,7 @@ class DeviceAuthorizationRequest:
|
|
|
20
20
|
patient_birth_date (str | Unset): Patient's date of birth, ISO YYYY-MM-DD.
|
|
21
21
|
patient_sex (str | Unset): Patient's sex.
|
|
22
22
|
patient_address_line1 (str | Unset): Patient's street address.
|
|
23
|
+
patient_address_line2 (str | Unset): Patient's street address, second line (apt, suite, unit).
|
|
23
24
|
patient_city (str | Unset): Patient's city.
|
|
24
25
|
patient_state (str | Unset): Patient's state or region.
|
|
25
26
|
patient_postal_code (str | Unset): Patient's postal code.
|
|
@@ -27,6 +28,8 @@ class DeviceAuthorizationRequest:
|
|
|
27
28
|
patient_email (str | Unset): Patient's email, used as the signer identity.
|
|
28
29
|
providers (str | Unset): JSON array of provider seeds to prefill the patient's list, each `{ "name", "phone"?
|
|
29
30
|
}`. Seeded by name (freeform); the patient can add, change, or remove them on the link page.
|
|
31
|
+
on_behalf_of_organization_name (str | Unset): Organization the verified requester is acting on behalf of for
|
|
32
|
+
this link.
|
|
30
33
|
"""
|
|
31
34
|
|
|
32
35
|
patient_ref: str | Unset = UNSET
|
|
@@ -35,12 +38,14 @@ class DeviceAuthorizationRequest:
|
|
|
35
38
|
patient_birth_date: str | Unset = UNSET
|
|
36
39
|
patient_sex: str | Unset = UNSET
|
|
37
40
|
patient_address_line1: str | Unset = UNSET
|
|
41
|
+
patient_address_line2: str | Unset = UNSET
|
|
38
42
|
patient_city: str | Unset = UNSET
|
|
39
43
|
patient_state: str | Unset = UNSET
|
|
40
44
|
patient_postal_code: str | Unset = UNSET
|
|
41
45
|
patient_phone: str | Unset = UNSET
|
|
42
46
|
patient_email: str | Unset = UNSET
|
|
43
47
|
providers: str | Unset = UNSET
|
|
48
|
+
on_behalf_of_organization_name: str | Unset = UNSET
|
|
44
49
|
|
|
45
50
|
def to_dict(self) -> dict[str, Any]:
|
|
46
51
|
patient_ref = self.patient_ref
|
|
@@ -55,6 +60,8 @@ class DeviceAuthorizationRequest:
|
|
|
55
60
|
|
|
56
61
|
patient_address_line1 = self.patient_address_line1
|
|
57
62
|
|
|
63
|
+
patient_address_line2 = self.patient_address_line2
|
|
64
|
+
|
|
58
65
|
patient_city = self.patient_city
|
|
59
66
|
|
|
60
67
|
patient_state = self.patient_state
|
|
@@ -67,6 +74,8 @@ class DeviceAuthorizationRequest:
|
|
|
67
74
|
|
|
68
75
|
providers = self.providers
|
|
69
76
|
|
|
77
|
+
on_behalf_of_organization_name = self.on_behalf_of_organization_name
|
|
78
|
+
|
|
70
79
|
field_dict: dict[str, Any] = {}
|
|
71
80
|
|
|
72
81
|
field_dict.update({})
|
|
@@ -82,6 +91,8 @@ class DeviceAuthorizationRequest:
|
|
|
82
91
|
field_dict["patient_sex"] = patient_sex
|
|
83
92
|
if patient_address_line1 is not UNSET:
|
|
84
93
|
field_dict["patient_address_line1"] = patient_address_line1
|
|
94
|
+
if patient_address_line2 is not UNSET:
|
|
95
|
+
field_dict["patient_address_line2"] = patient_address_line2
|
|
85
96
|
if patient_city is not UNSET:
|
|
86
97
|
field_dict["patient_city"] = patient_city
|
|
87
98
|
if patient_state is not UNSET:
|
|
@@ -94,6 +105,10 @@ class DeviceAuthorizationRequest:
|
|
|
94
105
|
field_dict["patient_email"] = patient_email
|
|
95
106
|
if providers is not UNSET:
|
|
96
107
|
field_dict["providers"] = providers
|
|
108
|
+
if on_behalf_of_organization_name is not UNSET:
|
|
109
|
+
field_dict["on_behalf_of_organization_name"] = (
|
|
110
|
+
on_behalf_of_organization_name
|
|
111
|
+
)
|
|
97
112
|
|
|
98
113
|
return field_dict
|
|
99
114
|
|
|
@@ -112,6 +127,8 @@ class DeviceAuthorizationRequest:
|
|
|
112
127
|
|
|
113
128
|
patient_address_line1 = d.pop("patient_address_line1", UNSET)
|
|
114
129
|
|
|
130
|
+
patient_address_line2 = d.pop("patient_address_line2", UNSET)
|
|
131
|
+
|
|
115
132
|
patient_city = d.pop("patient_city", UNSET)
|
|
116
133
|
|
|
117
134
|
patient_state = d.pop("patient_state", UNSET)
|
|
@@ -124,6 +141,8 @@ class DeviceAuthorizationRequest:
|
|
|
124
141
|
|
|
125
142
|
providers = d.pop("providers", UNSET)
|
|
126
143
|
|
|
144
|
+
on_behalf_of_organization_name = d.pop("on_behalf_of_organization_name", UNSET)
|
|
145
|
+
|
|
127
146
|
device_authorization_request = cls(
|
|
128
147
|
patient_ref=patient_ref,
|
|
129
148
|
patient_first_name=patient_first_name,
|
|
@@ -131,12 +150,14 @@ class DeviceAuthorizationRequest:
|
|
|
131
150
|
patient_birth_date=patient_birth_date,
|
|
132
151
|
patient_sex=patient_sex,
|
|
133
152
|
patient_address_line1=patient_address_line1,
|
|
153
|
+
patient_address_line2=patient_address_line2,
|
|
134
154
|
patient_city=patient_city,
|
|
135
155
|
patient_state=patient_state,
|
|
136
156
|
patient_postal_code=patient_postal_code,
|
|
137
157
|
patient_phone=patient_phone,
|
|
138
158
|
patient_email=patient_email,
|
|
139
159
|
providers=providers,
|
|
160
|
+
on_behalf_of_organization_name=on_behalf_of_organization_name,
|
|
140
161
|
)
|
|
141
162
|
|
|
142
163
|
return device_authorization_request
|
|
@@ -51,7 +51,9 @@ class OperationOutcome:
|
|
|
51
51
|
|
|
52
52
|
@classmethod
|
|
53
53
|
def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
|
|
54
|
-
from ..models.operation_outcome_issue_item import
|
|
54
|
+
from ..models.operation_outcome_issue_item import (
|
|
55
|
+
OperationOutcomeIssueItem,
|
|
56
|
+
)
|
|
55
57
|
|
|
56
58
|
d = dict(src_dict)
|
|
57
59
|
resource_type = cast(Literal["OperationOutcome"], d.pop("resourceType"))
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/patient_address.py
RENAMED
|
@@ -15,12 +15,14 @@ class PatientAddress:
|
|
|
15
15
|
"""
|
|
16
16
|
Attributes:
|
|
17
17
|
line1 (str | Unset): Street address.
|
|
18
|
+
line2 (str | Unset): Street address, second line (apt, suite, unit).
|
|
18
19
|
city (str | Unset): City.
|
|
19
20
|
state (str | Unset): State or region.
|
|
20
21
|
postal_code (str | Unset): Postal code.
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
line1: str | Unset = UNSET
|
|
25
|
+
line2: str | Unset = UNSET
|
|
24
26
|
city: str | Unset = UNSET
|
|
25
27
|
state: str | Unset = UNSET
|
|
26
28
|
postal_code: str | Unset = UNSET
|
|
@@ -28,6 +30,8 @@ class PatientAddress:
|
|
|
28
30
|
def to_dict(self) -> dict[str, Any]:
|
|
29
31
|
line1 = self.line1
|
|
30
32
|
|
|
33
|
+
line2 = self.line2
|
|
34
|
+
|
|
31
35
|
city = self.city
|
|
32
36
|
|
|
33
37
|
state = self.state
|
|
@@ -39,6 +43,8 @@ class PatientAddress:
|
|
|
39
43
|
field_dict.update({})
|
|
40
44
|
if line1 is not UNSET:
|
|
41
45
|
field_dict["line1"] = line1
|
|
46
|
+
if line2 is not UNSET:
|
|
47
|
+
field_dict["line2"] = line2
|
|
42
48
|
if city is not UNSET:
|
|
43
49
|
field_dict["city"] = city
|
|
44
50
|
if state is not UNSET:
|
|
@@ -53,6 +59,8 @@ class PatientAddress:
|
|
|
53
59
|
d = dict(src_dict)
|
|
54
60
|
line1 = d.pop("line1", UNSET)
|
|
55
61
|
|
|
62
|
+
line2 = d.pop("line2", UNSET)
|
|
63
|
+
|
|
56
64
|
city = d.pop("city", UNSET)
|
|
57
65
|
|
|
58
66
|
state = d.pop("state", UNSET)
|
|
@@ -61,6 +69,7 @@ class PatientAddress:
|
|
|
61
69
|
|
|
62
70
|
patient_address = cls(
|
|
63
71
|
line1=line1,
|
|
72
|
+
line2=line2,
|
|
64
73
|
city=city,
|
|
65
74
|
state=state,
|
|
66
75
|
postal_code=postal_code,
|
|
@@ -11,15 +11,20 @@ device-authorization flow, and refreshes consent tokens transparently.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import base64
|
|
14
|
+
import json
|
|
14
15
|
import time
|
|
15
16
|
from collections.abc import Callable, Mapping
|
|
16
17
|
from dataclasses import dataclass
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from io import BytesIO
|
|
20
|
+
from pathlib import Path
|
|
17
21
|
from typing import Any, TypeVar
|
|
18
22
|
from urllib.parse import unquote
|
|
19
23
|
|
|
20
24
|
import httpx
|
|
21
25
|
|
|
22
26
|
from ._generated.api.default import (
|
|
27
|
+
attach_client_authorization as _attach_client_authorization,
|
|
23
28
|
create_consent_link,
|
|
24
29
|
exchange_token,
|
|
25
30
|
get_consent as _get_consent,
|
|
@@ -28,6 +33,7 @@ from ._generated.api.default import (
|
|
|
28
33
|
)
|
|
29
34
|
from ._generated.client import AuthenticatedClient
|
|
30
35
|
from ._generated.models import (
|
|
36
|
+
AttachClientAuthorizationBody,
|
|
31
37
|
Consent,
|
|
32
38
|
ConsentLinkRequest,
|
|
33
39
|
ConsentList,
|
|
@@ -37,7 +43,7 @@ from ._generated.models import (
|
|
|
37
43
|
TokenRequestGrantType,
|
|
38
44
|
TokenResponse,
|
|
39
45
|
)
|
|
40
|
-
from ._generated.types import UNSET
|
|
46
|
+
from ._generated.types import UNSET, File
|
|
41
47
|
|
|
42
48
|
DEFAULT_BASE_URL = "https://portal.hubble.ai"
|
|
43
49
|
_DEVICE_CODE_GRANT = TokenRequestGrantType("urn:ietf:params:oauth:grant-type:device_code")
|
|
@@ -88,13 +94,16 @@ class Hubble:
|
|
|
88
94
|
self,
|
|
89
95
|
*,
|
|
90
96
|
patient_ref: str | None = None,
|
|
97
|
+
on_behalf_of_organization_name: str | None = None,
|
|
91
98
|
patient: Mapping[str, Any] | None = None,
|
|
92
99
|
providers: list[Mapping[str, Any]] | None = None,
|
|
93
100
|
) -> DeviceAuthorizationResponse:
|
|
94
101
|
"""Start the patient link flow. Send the patient to the returned
|
|
95
102
|
``verification_uri_complete``, then call `wait_for_authorization()`.
|
|
96
103
|
|
|
97
|
-
Optionally prefill the link. ``
|
|
104
|
+
Optionally prefill the link. ``on_behalf_of_organization_name`` identifies
|
|
105
|
+
the organization your verified Hubble organization represents for this
|
|
106
|
+
link. ``patient`` is a mapping of demographics
|
|
98
107
|
(``first_name``, ``last_name``, ``birth_date``, ``sex``, ``phone``,
|
|
99
108
|
``email``, and a nested ``address``); ``providers`` is a list of provider
|
|
100
109
|
seeds (each ``{"name": ..., "phone"?: ...}``) to prefill the patient's
|
|
@@ -102,6 +111,8 @@ class Hubble:
|
|
|
102
111
|
body: dict[str, Any] = {}
|
|
103
112
|
if patient_ref is not None:
|
|
104
113
|
body["patient_ref"] = patient_ref
|
|
114
|
+
if on_behalf_of_organization_name is not None:
|
|
115
|
+
body["on_behalf_of_organization_name"] = on_behalf_of_organization_name
|
|
105
116
|
if patient is not None:
|
|
106
117
|
body["patient"] = dict(patient)
|
|
107
118
|
if providers is not None:
|
|
@@ -172,6 +183,59 @@ class Hubble:
|
|
|
172
183
|
f"Could not revoke consent {consent_id}",
|
|
173
184
|
)
|
|
174
185
|
|
|
186
|
+
def attach_authorization(
|
|
187
|
+
self,
|
|
188
|
+
consent_id: str,
|
|
189
|
+
*,
|
|
190
|
+
authorization: bytes | str | Path,
|
|
191
|
+
disclosers: list[Mapping[str, str]],
|
|
192
|
+
expires_at: str | datetime | None = None,
|
|
193
|
+
content_type: str = "application/pdf",
|
|
194
|
+
) -> Consent:
|
|
195
|
+
"""Attach a customer-supplied, already-signed HIPAA authorization to a
|
|
196
|
+
consent and grant delegated retrieval from the providers it names.
|
|
197
|
+
|
|
198
|
+
The signed artifact is the evidence, so the consent is granted at once
|
|
199
|
+
with no patient step, and the granted `Consent` is returned — each
|
|
200
|
+
discloser appears on it as a one-time source, ``pending`` until its
|
|
201
|
+
records are retrieved. Requires the ``byo_hipaa_authorization``
|
|
202
|
+
entitlement on your organization (a caller without it gets a `HubbleError`
|
|
203
|
+
wrapping the API's 403).
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
authorization: the signed file to upload, as raw ``bytes`` or a
|
|
207
|
+
filesystem path (``str``/``Path``) to read it from.
|
|
208
|
+
disclosers: the providers to disclose from — a list of mappings, each
|
|
209
|
+
with a ``name`` (e.g. a provider named General Hospital),
|
|
210
|
+
serialized to the JSON array the API expects.
|
|
211
|
+
expires_at: the authorization's asserted expiration (ISO 8601 string
|
|
212
|
+
or ``datetime``); omit if it does not expire.
|
|
213
|
+
"""
|
|
214
|
+
if isinstance(authorization, (str, Path)):
|
|
215
|
+
path = Path(authorization)
|
|
216
|
+
data = path.read_bytes()
|
|
217
|
+
upload_name = path.name
|
|
218
|
+
else:
|
|
219
|
+
data = authorization
|
|
220
|
+
upload_name = "authorization"
|
|
221
|
+
|
|
222
|
+
expires = (
|
|
223
|
+
datetime.fromisoformat(expires_at) if isinstance(expires_at, str) else expires_at
|
|
224
|
+
)
|
|
225
|
+
# The multipart part must carry a non-empty filename or the server parses
|
|
226
|
+
# it as a text field instead of a file. The value is transport-only — the
|
|
227
|
+
# API stores the artifact under its own id and never reads this name.
|
|
228
|
+
body = AttachClientAuthorizationBody(
|
|
229
|
+
authorization=File(payload=BytesIO(data), file_name=upload_name, mime_type=content_type),
|
|
230
|
+
disclosers=json.dumps([dict(d) for d in disclosers]),
|
|
231
|
+
expires_at=expires if expires is not None else UNSET,
|
|
232
|
+
)
|
|
233
|
+
return _unwrap(
|
|
234
|
+
_attach_client_authorization.sync(id=consent_id, client=self._control, body=body),
|
|
235
|
+
Consent,
|
|
236
|
+
f"Could not attach authorization to consent {consent_id}",
|
|
237
|
+
)
|
|
238
|
+
|
|
175
239
|
# ── reconstruct a session from stored tokens ──────────────────────────────
|
|
176
240
|
|
|
177
241
|
def session(
|
|
@@ -15,7 +15,14 @@ import pytest
|
|
|
15
15
|
import respx
|
|
16
16
|
|
|
17
17
|
from hubble_sdk import ConsentTimeout, Hubble, HubbleError, TokenSet
|
|
18
|
-
from hubble_sdk._generated.models import
|
|
18
|
+
from hubble_sdk._generated.models import (
|
|
19
|
+
ConsentLinkRequest,
|
|
20
|
+
DeviceAuthorizationRequest,
|
|
21
|
+
DeviceAuthorizationResponse,
|
|
22
|
+
PatientDemographics,
|
|
23
|
+
ProviderSeed,
|
|
24
|
+
)
|
|
25
|
+
from hubble_sdk._generated.types import UNSET
|
|
19
26
|
|
|
20
27
|
BASE = "https://portal.hubble.ai"
|
|
21
28
|
BASIC = "Basic " + base64.b64encode(b"id:sec").decode()
|
|
@@ -41,7 +48,6 @@ CONSENT_DICT = {
|
|
|
41
48
|
"status": "active",
|
|
42
49
|
"patient_ref": "abc",
|
|
43
50
|
"created_at": "2026-01-01T00:00:00Z",
|
|
44
|
-
"granted_at": "2026-01-01T00:05:00Z",
|
|
45
51
|
"expires_at": "2026-06-01T00:00:00Z",
|
|
46
52
|
"sources": [{"provider": "General Hospital", "type": "live", "status": "available"}],
|
|
47
53
|
"error": None,
|
|
@@ -67,6 +73,7 @@ def test_create_link_posts_json_with_basic_auth_and_nested_prefill():
|
|
|
67
73
|
|
|
68
74
|
result = hub().create_link(
|
|
69
75
|
patient_ref="abc",
|
|
76
|
+
on_behalf_of_organization_name="Acme Health",
|
|
70
77
|
patient={"first_name": "Jane", "address": {"city": "Springfield"}},
|
|
71
78
|
providers=[{"name": "Dr. Smith", "phone": "555-0100"}],
|
|
72
79
|
)
|
|
@@ -77,11 +84,30 @@ def test_create_link_posts_json_with_basic_auth_and_nested_prefill():
|
|
|
77
84
|
assert req.headers["Content-Type"].startswith("application/json")
|
|
78
85
|
sent = json.loads(req.content)
|
|
79
86
|
assert sent["patient_ref"] == "abc"
|
|
87
|
+
assert sent["on_behalf_of_organization_name"] == "Acme Health"
|
|
80
88
|
assert sent["patient"]["first_name"] == "Jane"
|
|
81
89
|
assert sent["patient"]["address"]["city"] == "Springfield"
|
|
82
90
|
assert sent["providers"] == [{"name": "Dr. Smith", "phone": "555-0100"}]
|
|
83
91
|
|
|
84
92
|
|
|
93
|
+
def test_generated_request_models_preserve_existing_positional_arguments():
|
|
94
|
+
patient = PatientDemographics(first_name="Jane")
|
|
95
|
+
provider = ProviderSeed(name="Dr. Smith")
|
|
96
|
+
|
|
97
|
+
# These were valid positional calls before the represented-organization
|
|
98
|
+
# field was added. It must remain last in generated models so none of the
|
|
99
|
+
# existing arguments silently changes meaning.
|
|
100
|
+
link_request = ConsentLinkRequest("abc", patient, [provider])
|
|
101
|
+
form_request = DeviceAuthorizationRequest("abc", "Jane", "Rivera")
|
|
102
|
+
|
|
103
|
+
assert link_request.patient is patient
|
|
104
|
+
assert link_request.providers == [provider]
|
|
105
|
+
assert link_request.on_behalf_of_organization_name is UNSET
|
|
106
|
+
assert form_request.patient_first_name == "Jane"
|
|
107
|
+
assert form_request.patient_last_name == "Rivera"
|
|
108
|
+
assert form_request.on_behalf_of_organization_name is UNSET
|
|
109
|
+
|
|
110
|
+
|
|
85
111
|
@respx.mock
|
|
86
112
|
def test_create_link_raises_with_context_on_error():
|
|
87
113
|
respx.post(f"{BASE}/api/v1/consents/oauth/device_authorization").mock(
|
|
@@ -136,6 +162,78 @@ def test_revoke_consent_surfaces_failure():
|
|
|
136
162
|
hub().revoke_consent("cns_x")
|
|
137
163
|
|
|
138
164
|
|
|
165
|
+
GRANT_CONSENT_DICT = {
|
|
166
|
+
"id": "cns_1",
|
|
167
|
+
"status": "active",
|
|
168
|
+
"patient_ref": "abc",
|
|
169
|
+
"created_at": "2026-01-01T00:00:00Z",
|
|
170
|
+
"expires_at": "2027-01-01T00:00:00Z",
|
|
171
|
+
"sources": [
|
|
172
|
+
{"provider": "General Hospital", "type": "one_time", "status": "pending"},
|
|
173
|
+
{"provider": "City Clinic", "type": "one_time", "status": "pending"},
|
|
174
|
+
],
|
|
175
|
+
"error": None,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@respx.mock
|
|
180
|
+
def test_attach_authorization_posts_multipart_and_returns_consent():
|
|
181
|
+
route = respx.post(f"{BASE}/api/v1/consents/cns_1/authorization").mock(
|
|
182
|
+
return_value=httpx.Response(200, json=GRANT_CONSENT_DICT)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
consent = hub().attach_authorization(
|
|
186
|
+
"cns_1",
|
|
187
|
+
authorization=b"%PDF-1.7 signed",
|
|
188
|
+
disclosers=[{"name": "General Hospital"}, {"name": "City Clinic"}],
|
|
189
|
+
expires_at="2027-01-01T00:00:00",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Returns the granted consent, with each discloser as a pending one-time source.
|
|
193
|
+
assert consent.id == "cns_1"
|
|
194
|
+
assert consent.status.value == "active"
|
|
195
|
+
assert [(s.provider, s.type_.value, s.status.value) for s in consent.sources] == [
|
|
196
|
+
("General Hospital", "one_time", "pending"),
|
|
197
|
+
("City Clinic", "one_time", "pending"),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
req = route.calls.last.request
|
|
201
|
+
assert req.headers["Authorization"] == BASIC
|
|
202
|
+
assert req.headers["Content-Type"].startswith("multipart/form-data")
|
|
203
|
+
body = req.content
|
|
204
|
+
assert b'name="authorization"' in body
|
|
205
|
+
assert b"%PDF-1.7 signed" in body
|
|
206
|
+
# disclosers travels as a JSON array string.
|
|
207
|
+
assert b'[{"name": "General Hospital"}, {"name": "City Clinic"}]' in body
|
|
208
|
+
assert b"2027-01-01T00:00:00" in body
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@respx.mock
|
|
212
|
+
def test_attach_authorization_reads_a_file_path(tmp_path):
|
|
213
|
+
route = respx.post(f"{BASE}/api/v1/consents/cns_1/authorization").mock(
|
|
214
|
+
return_value=httpx.Response(200, json=GRANT_CONSENT_DICT)
|
|
215
|
+
)
|
|
216
|
+
pdf = tmp_path / "signed-auth.pdf"
|
|
217
|
+
pdf.write_bytes(b"%PDF-1.7 from disk")
|
|
218
|
+
|
|
219
|
+
hub().attach_authorization("cns_1", authorization=pdf, disclosers=[{"name": "X"}])
|
|
220
|
+
|
|
221
|
+
body = route.calls.last.request.content
|
|
222
|
+
assert b"%PDF-1.7 from disk" in body
|
|
223
|
+
# The path's name becomes the upload filename.
|
|
224
|
+
assert b'filename="signed-auth.pdf"' in body
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@respx.mock
|
|
228
|
+
def test_attach_authorization_surfaces_not_entitled_403():
|
|
229
|
+
respx.post(f"{BASE}/api/v1/consents/cns_1/authorization").mock(
|
|
230
|
+
return_value=httpx.Response(403, json={"error": "not enabled for this"})
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
with pytest.raises(HubbleError, match="Could not attach authorization to consent cns_1"):
|
|
234
|
+
hub().attach_authorization("cns_1", authorization=b"x", disclosers=[{"name": "X"}])
|
|
235
|
+
|
|
236
|
+
|
|
139
237
|
def test_base_url_trailing_slash_is_normalized():
|
|
140
238
|
assert Hubble("id", "sec", base_url="https://staging.hubble.ai/").base_url == (
|
|
141
239
|
"https://staging.hubble.ai"
|
hubble_sdk_python-0.0.3/hubble_sdk/_generated/models/attach_client_authorization_response_201.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Mapping
|
|
4
|
-
from typing import Any, Self, TypeVar, cast
|
|
5
|
-
|
|
6
|
-
from attrs import define as _attrs_define
|
|
7
|
-
from attrs import field as _attrs_field
|
|
8
|
-
|
|
9
|
-
from ..models.attach_client_authorization_response_201_status import (
|
|
10
|
-
AttachClientAuthorizationResponse201Status,
|
|
11
|
-
)
|
|
12
|
-
from ..types import UNSET, Unset
|
|
13
|
-
|
|
14
|
-
T = TypeVar("T", bound="AttachClientAuthorizationResponse201")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@_attrs_define
|
|
18
|
-
class AttachClientAuthorizationResponse201:
|
|
19
|
-
"""
|
|
20
|
-
Attributes:
|
|
21
|
-
consent_id (str | Unset):
|
|
22
|
-
status (AttachClientAuthorizationResponse201Status | Unset):
|
|
23
|
-
authorization_id (str | Unset):
|
|
24
|
-
requests (list[str] | Unset):
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
consent_id: str | Unset = UNSET
|
|
28
|
-
status: AttachClientAuthorizationResponse201Status | Unset = UNSET
|
|
29
|
-
authorization_id: str | Unset = UNSET
|
|
30
|
-
requests: list[str] | Unset = UNSET
|
|
31
|
-
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
32
|
-
|
|
33
|
-
def to_dict(self) -> dict[str, Any]:
|
|
34
|
-
consent_id = self.consent_id
|
|
35
|
-
|
|
36
|
-
status: str | Unset = UNSET
|
|
37
|
-
if not isinstance(self.status, Unset):
|
|
38
|
-
status = self.status.value
|
|
39
|
-
|
|
40
|
-
authorization_id = self.authorization_id
|
|
41
|
-
|
|
42
|
-
requests: list[str] | Unset = UNSET
|
|
43
|
-
if not isinstance(self.requests, Unset):
|
|
44
|
-
requests = self.requests
|
|
45
|
-
|
|
46
|
-
field_dict: dict[str, Any] = {}
|
|
47
|
-
field_dict.update(self.additional_properties)
|
|
48
|
-
field_dict.update({})
|
|
49
|
-
if consent_id is not UNSET:
|
|
50
|
-
field_dict["consent_id"] = consent_id
|
|
51
|
-
if status is not UNSET:
|
|
52
|
-
field_dict["status"] = status
|
|
53
|
-
if authorization_id is not UNSET:
|
|
54
|
-
field_dict["authorization_id"] = authorization_id
|
|
55
|
-
if requests is not UNSET:
|
|
56
|
-
field_dict["requests"] = requests
|
|
57
|
-
|
|
58
|
-
return field_dict
|
|
59
|
-
|
|
60
|
-
@classmethod
|
|
61
|
-
def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
|
|
62
|
-
d = dict(src_dict)
|
|
63
|
-
consent_id = d.pop("consent_id", UNSET)
|
|
64
|
-
|
|
65
|
-
_status = d.pop("status", UNSET)
|
|
66
|
-
status: AttachClientAuthorizationResponse201Status | Unset
|
|
67
|
-
if isinstance(_status, Unset):
|
|
68
|
-
status = UNSET
|
|
69
|
-
else:
|
|
70
|
-
status = AttachClientAuthorizationResponse201Status(_status)
|
|
71
|
-
|
|
72
|
-
authorization_id = d.pop("authorization_id", UNSET)
|
|
73
|
-
|
|
74
|
-
requests = cast(list[str], d.pop("requests", UNSET))
|
|
75
|
-
|
|
76
|
-
attach_client_authorization_response_201 = cls(
|
|
77
|
-
consent_id=consent_id,
|
|
78
|
-
status=status,
|
|
79
|
-
authorization_id=authorization_id,
|
|
80
|
-
requests=requests,
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
attach_client_authorization_response_201.additional_properties = d
|
|
84
|
-
return attach_client_authorization_response_201
|
|
85
|
-
|
|
86
|
-
@property
|
|
87
|
-
def additional_keys(self) -> list[str]:
|
|
88
|
-
return list(self.additional_properties.keys())
|
|
89
|
-
|
|
90
|
-
def __getitem__(self, key: str) -> Any:
|
|
91
|
-
return self.additional_properties[key]
|
|
92
|
-
|
|
93
|
-
def __setitem__(self, key: str, value: Any) -> None:
|
|
94
|
-
self.additional_properties[key] = value
|
|
95
|
-
|
|
96
|
-
def __delitem__(self, key: str) -> None:
|
|
97
|
-
del self.additional_properties[key]
|
|
98
|
-
|
|
99
|
-
def __contains__(self, key: str) -> bool:
|
|
100
|
-
return key in self.additional_properties
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/get_consent.py
RENAMED
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/api/default/read_fhir.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/api_error.py
RENAMED
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_error.py
RENAMED
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_list.py
RENAMED
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/consent_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/fhir_resource.py
RENAMED
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/o_auth_error.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/provider_seed.py
RENAMED
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/token_request.py
RENAMED
|
File without changes
|
{hubble_sdk_python-0.0.3 → hubble_sdk_python-0.1.0}/hubble_sdk/_generated/models/token_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|