eyesoft-api-client 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.
- eyesoft_api_client-0.1.0/PKG-INFO +125 -0
- eyesoft_api_client-0.1.0/README.md +111 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/__init__.py +4 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/_http.py +71 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/client.py +62 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/resources/__init__.py +4 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/resources/base.py +44 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/resources/exercise.py +13 -0
- eyesoft_api_client-0.1.0/eyesoft_api_client/resources/patient.py +14 -0
- eyesoft_api_client-0.1.0/pyproject.toml +17 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eyesoft-api-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Arnaud-Marie Gallardo
|
|
6
|
+
Author-email: arnaud.g@eyesoft.fr
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Eyesoft API Client
|
|
15
|
+
|
|
16
|
+
Python client for the Eyesoft API.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install eyesoft-api-client
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from eyesoft_api_client import Client
|
|
28
|
+
|
|
29
|
+
with Client(
|
|
30
|
+
base_url="https://api.example.com",
|
|
31
|
+
username="user",
|
|
32
|
+
password="pass",
|
|
33
|
+
client_id="client_id",
|
|
34
|
+
client_secret="client_secret",
|
|
35
|
+
) as client:
|
|
36
|
+
patients = client.list_patients()
|
|
37
|
+
print(patients[0].id)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Authentication
|
|
41
|
+
|
|
42
|
+
The client authenticates with the API using OAuth2 (username/password + client credentials). Tokens are managed automatically by the HTTP session.
|
|
43
|
+
|
|
44
|
+
| Parameter | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `base_url` | Root URL of the API (no trailing slash) |
|
|
47
|
+
| `username` | User account login |
|
|
48
|
+
| `password` | User account password |
|
|
49
|
+
| `client_id` | OAuth2 client ID |
|
|
50
|
+
| `client_secret` | OAuth2 client secret |
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Patients
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# List all patients
|
|
58
|
+
patients = client.list_patients()
|
|
59
|
+
|
|
60
|
+
# Fetch a single patient by UUID
|
|
61
|
+
patient = client.get_patient("550e8400-e29b-41d4-a716-446655440000")
|
|
62
|
+
|
|
63
|
+
# Access patient fields directly as attributes
|
|
64
|
+
print(patient.id, patient.name)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Exercises
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# List all exercises for a patient
|
|
71
|
+
exercises = patient.list_exercises()
|
|
72
|
+
|
|
73
|
+
# Fetch a single exercise for a patient
|
|
74
|
+
exercise = patient.get_exercise("550e8400-e29b-41d4-a716-446655440001")
|
|
75
|
+
|
|
76
|
+
# Fetch raw exercise data (parsed JSON)
|
|
77
|
+
data = exercise.get_data()
|
|
78
|
+
|
|
79
|
+
# Or fetch exercise data directly from the client
|
|
80
|
+
data = client.get_exercise_data("550e8400-e29b-41d4-a716-446655440001")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Resource objects
|
|
84
|
+
|
|
85
|
+
`Patient` and `Exercise` objects proxy attribute access to the underlying API response, so any field returned by the API is accessible directly:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
patient = client.get_patient("550e8400-e29b-41d4-a716-446655440000")
|
|
89
|
+
patient.id # any field in the JSON response
|
|
90
|
+
patient["name"] # dict-style access also works
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Lifecycle
|
|
94
|
+
|
|
95
|
+
Use the client as a context manager (recommended) to ensure the underlying HTTP session is closed:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
with Client(...) as client:
|
|
99
|
+
...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or manage it manually:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
client = Client(...)
|
|
106
|
+
try:
|
|
107
|
+
...
|
|
108
|
+
finally:
|
|
109
|
+
client.close()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
### Publishing
|
|
115
|
+
|
|
116
|
+
This project uses [python-semantic-release](https://python-semantic-release.readthedocs.io/) with [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning and publishing to PyPI via GitLab CI.
|
|
117
|
+
|
|
118
|
+
Releases are triggered automatically on every push to the default branch when the commit history contains releasable changes:
|
|
119
|
+
|
|
120
|
+
| Commit prefix | Version bump |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `fix: ...` | Patch (0.1.0 → 0.1.1) |
|
|
123
|
+
| `feat: ...` | Minor (0.1.0 → 0.2.0) |
|
|
124
|
+
| `feat!: ...` / `BREAKING CHANGE:` | Major (0.1.0 → 1.0.0) |
|
|
125
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Eyesoft API Client
|
|
2
|
+
|
|
3
|
+
Python client for the Eyesoft API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install eyesoft-api-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from eyesoft_api_client import Client
|
|
15
|
+
|
|
16
|
+
with Client(
|
|
17
|
+
base_url="https://api.example.com",
|
|
18
|
+
username="user",
|
|
19
|
+
password="pass",
|
|
20
|
+
client_id="client_id",
|
|
21
|
+
client_secret="client_secret",
|
|
22
|
+
) as client:
|
|
23
|
+
patients = client.list_patients()
|
|
24
|
+
print(patients[0].id)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Authentication
|
|
28
|
+
|
|
29
|
+
The client authenticates with the API using OAuth2 (username/password + client credentials). Tokens are managed automatically by the HTTP session.
|
|
30
|
+
|
|
31
|
+
| Parameter | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `base_url` | Root URL of the API (no trailing slash) |
|
|
34
|
+
| `username` | User account login |
|
|
35
|
+
| `password` | User account password |
|
|
36
|
+
| `client_id` | OAuth2 client ID |
|
|
37
|
+
| `client_secret` | OAuth2 client secret |
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Patients
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# List all patients
|
|
45
|
+
patients = client.list_patients()
|
|
46
|
+
|
|
47
|
+
# Fetch a single patient by UUID
|
|
48
|
+
patient = client.get_patient("550e8400-e29b-41d4-a716-446655440000")
|
|
49
|
+
|
|
50
|
+
# Access patient fields directly as attributes
|
|
51
|
+
print(patient.id, patient.name)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Exercises
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# List all exercises for a patient
|
|
58
|
+
exercises = patient.list_exercises()
|
|
59
|
+
|
|
60
|
+
# Fetch a single exercise for a patient
|
|
61
|
+
exercise = patient.get_exercise("550e8400-e29b-41d4-a716-446655440001")
|
|
62
|
+
|
|
63
|
+
# Fetch raw exercise data (parsed JSON)
|
|
64
|
+
data = exercise.get_data()
|
|
65
|
+
|
|
66
|
+
# Or fetch exercise data directly from the client
|
|
67
|
+
data = client.get_exercise_data("550e8400-e29b-41d4-a716-446655440001")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Resource objects
|
|
71
|
+
|
|
72
|
+
`Patient` and `Exercise` objects proxy attribute access to the underlying API response, so any field returned by the API is accessible directly:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
patient = client.get_patient("550e8400-e29b-41d4-a716-446655440000")
|
|
76
|
+
patient.id # any field in the JSON response
|
|
77
|
+
patient["name"] # dict-style access also works
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Lifecycle
|
|
81
|
+
|
|
82
|
+
Use the client as a context manager (recommended) to ensure the underlying HTTP session is closed:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
with Client(...) as client:
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or manage it manually:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client = Client(...)
|
|
93
|
+
try:
|
|
94
|
+
...
|
|
95
|
+
finally:
|
|
96
|
+
client.close()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
### Publishing
|
|
102
|
+
|
|
103
|
+
This project uses [python-semantic-release](https://python-semantic-release.readthedocs.io/) with [Conventional Commits](https://www.conventionalcommits.org/) for automated versioning and publishing to PyPI via GitLab CI.
|
|
104
|
+
|
|
105
|
+
Releases are triggered automatically on every push to the default branch when the commit history contains releasable changes:
|
|
106
|
+
|
|
107
|
+
| Commit prefix | Version bump |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `fix: ...` | Patch (0.1.0 → 0.1.1) |
|
|
110
|
+
| `feat: ...` | Minor (0.1.0 → 0.2.0) |
|
|
111
|
+
| `feat!: ...` / `BREAKING CHANGE:` | Major (0.1.0 → 1.0.0) |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HTTPSession:
|
|
7
|
+
"""Thin wrapper around httpx.Client that holds auth and base URL."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, base_url: str, username: str, password: str, client_id: str, client_secret: str) -> None:
|
|
10
|
+
self._client = httpx.Client(
|
|
11
|
+
base_url=base_url,
|
|
12
|
+
headers={"Accept": "application/json"},
|
|
13
|
+
)
|
|
14
|
+
self._login(username, password, client_id, client_secret)
|
|
15
|
+
|
|
16
|
+
# ------------------------------------------------------------------
|
|
17
|
+
# HTTP verbs
|
|
18
|
+
# ------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
def get(self, path: str, **kwargs: Any) -> Any:
|
|
21
|
+
return self._request("GET", path, **kwargs)
|
|
22
|
+
|
|
23
|
+
def post(self, path: str, **kwargs: Any) -> Any:
|
|
24
|
+
return self._request("POST", path, **kwargs)
|
|
25
|
+
|
|
26
|
+
def put(self, path: str, **kwargs: Any) -> Any:
|
|
27
|
+
return self._request("PUT", path, **kwargs)
|
|
28
|
+
|
|
29
|
+
def patch(self, path: str, **kwargs: Any) -> Any:
|
|
30
|
+
return self._request("PATCH", path, **kwargs)
|
|
31
|
+
|
|
32
|
+
def delete(self, path: str, **kwargs: Any) -> Any:
|
|
33
|
+
return self._request("DELETE", path, **kwargs)
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------------
|
|
36
|
+
# Internal
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
40
|
+
response = self._client.request(method, path, **kwargs)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
if response.content:
|
|
43
|
+
return response.json()
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def _login(self, username: str, password: str, client_id: str, client_secret: str) -> None:
|
|
47
|
+
token = self.post(
|
|
48
|
+
"/oauth/v2/token",
|
|
49
|
+
data={
|
|
50
|
+
"grant_type": "password",
|
|
51
|
+
"scope": "SCOPE_USER",
|
|
52
|
+
"username": username,
|
|
53
|
+
"password": password,
|
|
54
|
+
"client_id": client_id,
|
|
55
|
+
"client_secret": client_secret,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
self._client.headers["Authorization"] = f"Bearer {token['access_token']}"
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
# Context manager / cleanup
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def close(self) -> None:
|
|
65
|
+
self._client.close()
|
|
66
|
+
|
|
67
|
+
def __enter__(self) -> "HTTPSession":
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, *args: Any) -> None:
|
|
71
|
+
self.close()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from ._http import HTTPSession
|
|
4
|
+
from .resources.patient import Patient
|
|
5
|
+
from .resources.exercise import Exercise
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Client:
|
|
11
|
+
"""
|
|
12
|
+
Entry point for the Eyesoft API.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
client = Client("https://api.example.com", "user", "pass", "client_id", "client_secret")
|
|
17
|
+
patients = client.list_patients()
|
|
18
|
+
exercises = patients[0].list_exercises()
|
|
19
|
+
client.close()
|
|
20
|
+
|
|
21
|
+
Or as a context manager::
|
|
22
|
+
|
|
23
|
+
with Client("https://api.example.com", "user", "pass", "client_id", "client_secret") as client:
|
|
24
|
+
patients = client.list_patients()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, username: str, password: str, client_id: str, client_secret: str) -> None:
|
|
28
|
+
self._http = HTTPSession(base_url, username, password, client_id, client_secret)
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Patients
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def list_patients(self) -> list[Patient]:
|
|
35
|
+
data = self._http.get("/api/patients")
|
|
36
|
+
return [Patient(item, self._http) for item in data]
|
|
37
|
+
|
|
38
|
+
def get_patient(self, patient_id: str) -> Patient:
|
|
39
|
+
data = self._http.get(f"/api/patients/{patient_id}")
|
|
40
|
+
return Patient(data, self._http)
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# Exercises
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def get_exercise_data(self, exercise_id: str) -> Any:
|
|
47
|
+
data = self._http.get(f"/api/exercices/{exercise_id}/data")
|
|
48
|
+
parsed_data = json.loads(data)
|
|
49
|
+
return parsed_data
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
# Context manager / cleanup
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
self._http.close()
|
|
57
|
+
|
|
58
|
+
def __enter__(self) -> "Client":
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def __exit__(self, *args: object) -> None:
|
|
62
|
+
self.close()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from eyesoft_api_client._http import HTTPSession
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseResource:
|
|
8
|
+
"""
|
|
9
|
+
Base class for all API resource objects.
|
|
10
|
+
|
|
11
|
+
Subclasses receive raw JSON data from the API and a shared HTTP session.
|
|
12
|
+
Attribute access is forwarded to the underlying data dict, so you can
|
|
13
|
+
write ``patient.id`` instead of ``patient._data["id"]``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, data: dict[str, Any], http: "HTTPSession") -> None:
|
|
17
|
+
# Use object.__setattr__ to avoid triggering our own __setattr__
|
|
18
|
+
object.__setattr__(self, "_data", data)
|
|
19
|
+
object.__setattr__(self, "_http", http)
|
|
20
|
+
|
|
21
|
+
# ------------------------------------------------------------------
|
|
22
|
+
# Attribute / item access
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
def __getattr__(self, name: str) -> Any:
|
|
26
|
+
data = object.__getattribute__(self, "_data")
|
|
27
|
+
try:
|
|
28
|
+
return data[name]
|
|
29
|
+
except KeyError:
|
|
30
|
+
raise AttributeError(
|
|
31
|
+
f"'{type(self).__name__}' has no attribute '{name}'"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __getitem__(self, key: str) -> Any:
|
|
35
|
+
return object.__getattribute__(self, "_data")[key]
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# Representation
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
data = object.__getattribute__(self, "_data")
|
|
43
|
+
preview = {k: v for k, v in list(data.items())[:3]}
|
|
44
|
+
return f"{type(self).__name__}({preview})"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .base import BaseResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Exercise(BaseResource):
|
|
8
|
+
"""Represents a single exercise returned by the API."""
|
|
9
|
+
|
|
10
|
+
def get_data(self) -> Any:
|
|
11
|
+
data = self._http.get(f"/api/exercices/{self.id}/data")
|
|
12
|
+
parsed_data = json.loads(data)
|
|
13
|
+
return parsed_data
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .base import BaseResource
|
|
2
|
+
from .exercise import Exercise
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Patient(BaseResource):
|
|
6
|
+
"""Represents a single patient returned by the API."""
|
|
7
|
+
|
|
8
|
+
def list_exercises(self) -> list[Exercise]:
|
|
9
|
+
data = self._http.get(f"/api/patients/{self.id}/stats/history/evaluation")
|
|
10
|
+
return [Exercise(item, self._http) for item in data]
|
|
11
|
+
|
|
12
|
+
def get_exercise(self, exercise_id: str) -> Exercise:
|
|
13
|
+
data = self._http.get(f"/patients/{self.id}/exercises/{exercise_id}")
|
|
14
|
+
return Exercise(data, self._http)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "eyesoft-api-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Arnaud-Marie Gallardo",email = "arnaud.g@eyesoft.fr"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx (>=0.28.1,<0.29.0)"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
17
|
+
build-backend = "poetry.core.masonry.api"
|