Skip to content

Commit 4dc260d

Browse files
committed
Add basic interaction with PhysioNet API.
1 parent 3982ad9 commit 4dc260d

File tree

15 files changed

+1141
-5
lines changed

15 files changed

+1141
-5
lines changed

README.md

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,104 @@ pip install physionet
1010

1111
## Usage
1212

13+
### API Client
14+
15+
Interact with the PhysioNet API to explore and search published projects:
16+
17+
```python
18+
from physionet import PhysioNetClient
19+
20+
# Create a client instance
21+
client = PhysioNetClient()
22+
23+
# List all published projects
24+
projects = client.projects.list_published()
25+
print(f"Total projects: {len(projects)}")
26+
27+
# Display first few projects
28+
for project in projects[:5]:
29+
print(f"{project.slug} v{project.version}: {project.title}")
30+
31+
# Search for projects
32+
ecg_projects = client.projects.search('ECG')
33+
print(f"Found {len(ecg_projects)} ECG-related projects")
34+
35+
# Get all versions of a project
36+
versions = client.projects.list_versions('mimic-iv-demo')
37+
for version in versions:
38+
print(f"Version {version.version}: {version.title}")
39+
40+
# Get detailed information about a specific version
41+
details = client.projects.get_details('mimic-iv-demo', '2.2')
42+
print(f"Title: {details.title}")
43+
print(f"DOI: {details.doi}")
44+
print(f"Published: {details.publish_datetime}")
45+
print(f"Size: {details.main_storage_size} bytes")
46+
```
47+
48+
### Authenticated Requests
49+
50+
For endpoints that require authentication (e.g., downloading checksums):
51+
52+
```python
53+
from physionet import PhysioNetClient
54+
55+
# Create client with authentication
56+
client = PhysioNetClient(
57+
username='your_username',
58+
password='your_password'
59+
)
60+
61+
# Download checksums file
62+
client.projects.download_checksums(
63+
'mimic-iv-demo',
64+
'2.2',
65+
'checksums.txt'
66+
)
67+
68+
# Or use environment variables
69+
# Set PHYSIONET_USERNAME and PHYSIONET_PASSWORD
70+
from physionet.api.utils import get_credentials_from_env
71+
72+
username, password = get_credentials_from_env()
73+
client = PhysioNetClient(username=username, password=password)
74+
```
75+
76+
### Using Context Manager
77+
78+
```python
79+
from physionet import PhysioNetClient
80+
81+
# Automatically close session when done
82+
with PhysioNetClient() as client:
83+
projects = client.projects.list_published()
84+
print(f"Found {len(projects)} projects")
85+
```
86+
87+
### Utility Functions
88+
89+
```python
90+
from physionet.api.utils import format_size
91+
92+
# Format bytes to human-readable size
93+
size = format_size(16224447)
94+
print(size) # "15.47 MB"
95+
```
96+
97+
## Error Handling
98+
1399
```python
14-
import physionet as pn
100+
from physionet import PhysioNetClient
101+
from physionet.api.exceptions import NotFoundError, RateLimitError, ForbiddenError
15102

16-
# Download a dataset
17-
pn.download('ptbdb', 'data/ptbdb')
103+
client = PhysioNetClient()
18104

19-
# List all datasets
20-
pn.list_datasets()
105+
try:
106+
details = client.projects.get_details('nonexistent-project', '1.0')
107+
except NotFoundError:
108+
print("Project not found")
109+
except RateLimitError:
110+
print("Rate limit exceeded, please wait before retrying")
111+
except ForbiddenError:
112+
print("Access denied - check credentials or project permissions")
21113
```

physionet/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .api import PhysioNetClient
2+
3+
__all__ = ["PhysioNetClient"]

physionet/api/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from .client import PhysioNetClient
2+
from .exceptions import (
3+
PhysioNetAPIError,
4+
BadRequestError,
5+
ForbiddenError,
6+
NotFoundError,
7+
RateLimitError,
8+
)
9+
10+
__all__ = [
11+
"PhysioNetClient",
12+
"PhysioNetAPIError",
13+
"BadRequestError",
14+
"ForbiddenError",
15+
"NotFoundError",
16+
"RateLimitError",
17+
]

physionet/api/client.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import requests
2+
from typing import Optional, Dict, Any
3+
from urllib.parse import urljoin
4+
5+
from .exceptions import (
6+
PhysioNetAPIError,
7+
BadRequestError,
8+
ForbiddenError,
9+
NotFoundError,
10+
RateLimitError,
11+
)
12+
from .endpoints import ProjectsAPI
13+
14+
15+
class PhysioNetClient:
16+
"""Main client for interacting with PhysioNet API v1."""
17+
18+
def __init__(
19+
self,
20+
base_url: str = "https://physionet.org",
21+
username: Optional[str] = None,
22+
password: Optional[str] = None,
23+
timeout: int = 30,
24+
):
25+
"""
26+
Initialize PhysioNet API client.
27+
28+
Args:
29+
base_url: Base URL for PhysioNet (default: https://physionet.org)
30+
username: Optional username for authenticated requests
31+
password: Optional password for authenticated requests
32+
timeout: Request timeout in seconds
33+
"""
34+
self.base_url = base_url.rstrip("/")
35+
self.api_base = f"{self.base_url}/api/v1/"
36+
self.timeout = timeout
37+
self.session = requests.Session()
38+
39+
if username and password:
40+
self.session.auth = (username, password)
41+
42+
self.session.headers.update({"User-Agent": "PhysioNet-Python-Client/1.0", "Accept": "application/json"})
43+
44+
self.projects = ProjectsAPI(self)
45+
46+
def _make_request(
47+
self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs
48+
) -> requests.Response:
49+
"""
50+
Make HTTP request to API.
51+
52+
Args:
53+
method: HTTP method (GET, POST, etc.)
54+
endpoint: API endpoint path
55+
params: Query parameters
56+
**kwargs: Additional arguments for requests
57+
58+
Returns:
59+
Response object
60+
61+
Raises:
62+
PhysioNetAPIError: On API errors
63+
requests.RequestException: On network errors
64+
"""
65+
url = urljoin(self.api_base, endpoint)
66+
67+
response = self.session.request(method=method, url=url, params=params, timeout=self.timeout, **kwargs)
68+
69+
if response.status_code >= 400:
70+
self._handle_error(response)
71+
72+
return response
73+
74+
def _handle_error(self, response: requests.Response):
75+
"""Handle API error responses."""
76+
try:
77+
error_data = response.json()
78+
error_msg = error_data.get("error", str(error_data))
79+
except Exception:
80+
error_msg = response.text or response.reason
81+
82+
if response.status_code == 400:
83+
raise BadRequestError(error_msg)
84+
elif response.status_code == 403:
85+
raise ForbiddenError(error_msg)
86+
elif response.status_code == 404:
87+
raise NotFoundError(error_msg)
88+
elif response.status_code == 429:
89+
raise RateLimitError(error_msg)
90+
else:
91+
raise PhysioNetAPIError(f"HTTP {response.status_code}: {error_msg}")
92+
93+
def close(self):
94+
"""Close the session."""
95+
self.session.close()
96+
97+
def __enter__(self):
98+
return self
99+
100+
def __exit__(self, exc_type, exc_val, exc_tb):
101+
self.close()

physionet/api/endpoints.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
from typing import List, Optional, Iterator
2+
from physionet.api.models import PublishedProject, ProjectVersion, ProjectDetail
3+
4+
5+
class ProjectsAPI:
6+
"""API methods for interacting with projects."""
7+
8+
def __init__(self, client):
9+
self.client = client
10+
11+
def list_published(self) -> List[PublishedProject]:
12+
"""
13+
List all published projects.
14+
15+
Returns:
16+
List of PublishedProject objects
17+
18+
Note:
19+
The API returns all projects in a single response (no pagination).
20+
"""
21+
response = self.client._make_request("GET", "projects/published/")
22+
data = response.json()
23+
24+
return [PublishedProject.from_dict(p) for p in data]
25+
26+
def iter_published(self) -> Iterator[PublishedProject]:
27+
"""
28+
Iterator that yields all published projects.
29+
30+
Yields:
31+
PublishedProject objects
32+
33+
Note:
34+
This is a convenience method that iterates over list_published() results.
35+
"""
36+
for project in self.list_published():
37+
yield project
38+
39+
def search(self, search_term: str, resource_type: Optional[List[str]] = None) -> List[PublishedProject]:
40+
"""
41+
Search published projects.
42+
43+
Args:
44+
search_term: Search keywords
45+
resource_type: Filter by resource type(s), or ['all'] for all types
46+
47+
Returns:
48+
List of matching PublishedProject objects
49+
"""
50+
params = {"search_term": search_term}
51+
52+
if resource_type:
53+
params["resource_type"] = resource_type
54+
55+
response = self.client._make_request("GET", "projects/search/", params=params)
56+
data = response.json()
57+
58+
return [PublishedProject.from_dict(p) for p in data]
59+
60+
def list_versions(self, project_slug: str) -> List[ProjectVersion]:
61+
"""
62+
List all versions of a project.
63+
64+
Args:
65+
project_slug: Project identifier
66+
67+
Returns:
68+
List of ProjectVersion objects
69+
"""
70+
endpoint = f"projects/{project_slug}/versions/"
71+
response = self.client._make_request("GET", endpoint)
72+
data = response.json()
73+
74+
return [
75+
ProjectVersion(
76+
slug=v["slug"],
77+
title=v["title"],
78+
version=v["version"],
79+
abstract=v["abstract"],
80+
citation=v["citation"],
81+
)
82+
for v in data
83+
]
84+
85+
def get_details(self, project_slug: str, version: str) -> ProjectDetail:
86+
"""
87+
Get detailed information about a specific project version.
88+
89+
Args:
90+
project_slug: Project identifier
91+
version: Version number
92+
93+
Returns:
94+
ProjectDetail object
95+
"""
96+
endpoint = f"projects/{project_slug}/versions/{version}/"
97+
response = self.client._make_request("GET", endpoint)
98+
data = response.json()
99+
100+
return ProjectDetail.from_dict(data)
101+
102+
def download_checksums(self, project_slug: str, version: str, output_path: str):
103+
"""
104+
Download SHA256 checksums file for a project.
105+
106+
Args:
107+
project_slug: Project identifier
108+
version: Version number
109+
output_path: Path to save the checksums file
110+
111+
Note:
112+
Requires authentication and project access permissions.
113+
"""
114+
endpoint = f"projects/published/{project_slug}/{version}/sha256sums/"
115+
response = self.client._make_request("GET", endpoint)
116+
117+
with open(output_path, "wb") as f:
118+
f.write(response.content)

physionet/api/exceptions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class PhysioNetAPIError(Exception):
2+
"""Base exception for PhysioNet API errors."""
3+
4+
pass
5+
6+
7+
class BadRequestError(PhysioNetAPIError):
8+
"""Raised when API returns 400 Bad Request."""
9+
10+
pass
11+
12+
13+
class ForbiddenError(PhysioNetAPIError):
14+
"""Raised when API returns 403 Forbidden."""
15+
16+
pass
17+
18+
19+
class NotFoundError(PhysioNetAPIError):
20+
"""Raised when API returns 404 Not Found."""
21+
22+
pass
23+
24+
25+
class RateLimitError(PhysioNetAPIError):
26+
"""Raised when API returns 429 Too Many Requests."""
27+
28+
pass

0 commit comments

Comments
 (0)