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.
@@ -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,4 @@
1
+ from .client import Client
2
+ from .resources import Exercise, Patient
3
+
4
+ __all__ = ["Client", "Exercise", "Patient"]
@@ -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,4 @@
1
+ from .exercise import Exercise
2
+ from .patient import Patient
3
+
4
+ __all__ = ["Exercise", "Patient"]
@@ -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"