klokku-python-client 0.1.0__py3-none-any.whl

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,35 @@
1
+ from .api_client import (
2
+ # Exception classes
3
+ KlokkuApiError,
4
+ KlokkuAuthenticationError,
5
+ KlokkuNetworkError,
6
+ KlokkuApiResponseError,
7
+ KlokkuDataParsingError,
8
+ KlokkuDataStructureError,
9
+
10
+ # Data classes
11
+ Budget,
12
+ User,
13
+ Event,
14
+
15
+ # Main API client
16
+ KlokkuApi,
17
+ )
18
+
19
+ __all__ = [
20
+ # Exception classes
21
+ 'KlokkuApiError',
22
+ 'KlokkuAuthenticationError',
23
+ 'KlokkuNetworkError',
24
+ 'KlokkuApiResponseError',
25
+ 'KlokkuDataParsingError',
26
+ 'KlokkuDataStructureError',
27
+
28
+ # Data classes
29
+ 'Budget',
30
+ 'User',
31
+ 'Event',
32
+
33
+ # Main API client
34
+ 'KlokkuApi',
35
+ ]
@@ -0,0 +1,304 @@
1
+ import aiohttp
2
+ import logging
3
+ from typing import Optional
4
+
5
+ from dataclasses import dataclass
6
+
7
+ _LOGGER = logging.getLogger(__name__)
8
+
9
+ class KlokkuApiError(Exception):
10
+ """Base exception for all Klokku API errors."""
11
+ pass
12
+
13
+ class KlokkuAuthenticationError(KlokkuApiError):
14
+ """Raised when authentication fails or user is not authenticated."""
15
+ pass
16
+
17
+ class KlokkuNetworkError(KlokkuApiError):
18
+ """Raised when there's a network-related error."""
19
+ pass
20
+
21
+ class KlokkuApiResponseError(KlokkuApiError):
22
+ """Raised when the API returns an error response."""
23
+ def __init__(self, status_code: int, message: str = None):
24
+ self.status_code = status_code
25
+ self.message = message
26
+ super().__init__(f"API returned error {status_code}: {message}")
27
+
28
+ class KlokkuDataParsingError(KlokkuApiError):
29
+ """Raised when there's an error parsing the API response data."""
30
+ pass
31
+
32
+ class KlokkuDataStructureError(KlokkuApiError):
33
+ """Raised when the API response data doesn't have the expected structure."""
34
+ pass
35
+
36
+ @dataclass(frozen=True)
37
+ class Budget:
38
+ id: int
39
+ name: str
40
+ weeklyTime: int
41
+ icon: str = ""
42
+ status: str = "ACTIVE"
43
+
44
+ @dataclass(frozen=True)
45
+ class User:
46
+ id: int
47
+ username: str
48
+ display_name: str
49
+
50
+ @dataclass(frozen=True)
51
+ class Event:
52
+ id: int
53
+ startTime: str
54
+ budget: Budget
55
+
56
+ class KlokkuApi:
57
+
58
+ url: str = ""
59
+ username: str = ""
60
+ user_id: int = 0
61
+ session: Optional[aiohttp.ClientSession] = None
62
+
63
+ def __init__(self, url):
64
+ self.url = url
65
+ self.session = None
66
+
67
+ async def __aenter__(self):
68
+ self.session = aiohttp.ClientSession()
69
+ return self
70
+
71
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
72
+ if self.session:
73
+ await self.session.close()
74
+ self.session = None
75
+
76
+ async def authenticate(self, username: str) -> bool:
77
+ """
78
+ Authenticate with the API using a username.
79
+ :param username: The username to authenticate with.
80
+ :return: True if authentication was successful, False otherwise.
81
+ :raises KlokkuNetworkError: If there's a network error.
82
+ :raises KlokkuApiResponseError: If the API returns an error response.
83
+ :raises KlokkuDataParsingError: If there's an error when parsing the response.
84
+ :raises KlokkuDataStructureError: If the response doesn't have the expected structure.
85
+ """
86
+ try:
87
+ users = await self.get_users()
88
+ if not users:
89
+ return False
90
+ for user in users:
91
+ if user.username == username:
92
+ self.user_id = user.id
93
+ return True
94
+ return False
95
+ except KlokkuApiError as e:
96
+ _LOGGER.error(f"Authentication error: {e}")
97
+ return False
98
+
99
+ @staticmethod
100
+ def __headers(user_id: int) -> dict:
101
+ return {
102
+ "X-User-Id": str(user_id)
103
+ }
104
+
105
+ async def get_current_event(self) -> Event | None:
106
+ """
107
+ Fetch the current budget from the API.
108
+ :return: Parsed current budget data as a dictionary.
109
+ :raises KlokkuAuthenticationError: If the user is not authenticated.
110
+ :raises KlokkuNetworkError: If there's a network error.
111
+ :raises KlokkuApiResponseError: If the API returns an error response.
112
+ :raises KlokkuDataParsingError: If there's an error when parsing the response.
113
+ :raises KlokkuDataStructureError: If the response doesn't have the expected structure.
114
+ """
115
+ if not self.user_id:
116
+ error = KlokkuAuthenticationError("Unauthenticated - cannot fetch current budget")
117
+ _LOGGER.warning(str(error))
118
+ return None
119
+
120
+ url = f"{self.url}api/event/current"
121
+ try:
122
+ # Create a session if one doesn't exist
123
+ if not self.session:
124
+ self.session = aiohttp.ClientSession()
125
+ close_after = True
126
+ else:
127
+ close_after = False
128
+
129
+ try:
130
+ async with self.session.get(url, headers=self.__headers(self.user_id)) as response:
131
+ if response.status >= 400:
132
+ error_msg = await response.text()
133
+ raise KlokkuApiResponseError(response.status, error_msg)
134
+
135
+ try:
136
+ data = await response.json()
137
+ except aiohttp.ClientResponseError as e:
138
+ raise KlokkuDataParsingError(f"Failed to parse JSON response: {e}")
139
+
140
+ try:
141
+ result = Event(
142
+ id=data["id"],
143
+ startTime=data["startTime"],
144
+ budget=Budget(**data["budget"]),
145
+ )
146
+ except (KeyError, TypeError, ValueError) as e:
147
+ raise KlokkuDataStructureError(f"Unexpected data structure in response: {e}")
148
+ except aiohttp.ClientConnectionError as e:
149
+ raise KlokkuNetworkError(f"Connection error: {e}")
150
+
151
+ # Close the session if we created it in this method
152
+ if close_after:
153
+ await self.session.close()
154
+ self.session = None
155
+
156
+ return result
157
+ except KlokkuApiError as e:
158
+ _LOGGER.error(f"Error fetching current budget: {e}")
159
+ return None
160
+
161
+ async def get_all_budgets(self) -> list[Budget] | None:
162
+ """
163
+ Fetch all budgets from the API.
164
+ :return: Parsed list of all budgets.
165
+ :raises KlokkuAuthenticationError: If the user is not authenticated.
166
+ :raises KlokkuNetworkError: If there's a network error.
167
+ :raises KlokkuApiResponseError: If the API returns an error response.
168
+ :raises KlokkuDataParsingError: If there's an error when parsing the response.
169
+ :raises KlokkuDataStructureError: If the response doesn't have the expected structure.
170
+ """
171
+ if not self.user_id:
172
+ error = KlokkuAuthenticationError("Unauthenticated - cannot fetch budgets")
173
+ _LOGGER.warning(str(error))
174
+ return None
175
+
176
+ url = f"{self.url}api/budget"
177
+ try:
178
+ # Create a session if one doesn't exist
179
+ if not self.session:
180
+ self.session = aiohttp.ClientSession()
181
+ close_after = True
182
+ else:
183
+ close_after = False
184
+
185
+ try:
186
+ async with self.session.get(url, headers=self.__headers(self.user_id)) as response:
187
+ if response.status >= 400:
188
+ error_msg = await response.text()
189
+ raise KlokkuApiResponseError(response.status, error_msg)
190
+
191
+ try:
192
+ data = await response.json()
193
+ except aiohttp.ClientResponseError as e:
194
+ raise KlokkuDataParsingError(f"Failed to parse JSON response: {e}")
195
+
196
+ try:
197
+ result = [Budget(**budget) for budget in data]
198
+ except (KeyError, TypeError, ValueError) as e:
199
+ raise KlokkuDataStructureError(f"Unexpected data structure in response: {e}")
200
+ except aiohttp.ClientConnectionError as e:
201
+ raise KlokkuNetworkError(f"Connection error: {e}")
202
+
203
+ # Close the session if we created it in this method
204
+ if close_after:
205
+ await self.session.close()
206
+ self.session = None
207
+
208
+ return result
209
+ except KlokkuApiError as e:
210
+ _LOGGER.error(f"Error fetching all budgets: {e}")
211
+ return None
212
+
213
+ async def get_users(self) -> list[User] | None:
214
+ """
215
+ Fetch all users from the API.
216
+ :return: Parsed list of all users.
217
+ :raises KlokkuNetworkError: If there's a network error.
218
+ :raises KlokkuApiResponseError: If the API returns an error response.
219
+ :raises KlokkuDataParsingError: If there's an error when parsing the response.
220
+ :raises KlokkuDataStructureError: If the response doesn't have the expected structure.
221
+ """
222
+ url = f"{self.url}api/user"
223
+ try:
224
+ # Create a session if one doesn't exist
225
+ if not self.session:
226
+ self.session = aiohttp.ClientSession()
227
+ close_after = True
228
+ else:
229
+ close_after = False
230
+
231
+ try:
232
+ async with self.session.get(url) as response:
233
+ if response.status >= 400:
234
+ error_msg = await response.text()
235
+ raise KlokkuApiResponseError(response.status, error_msg)
236
+
237
+ try:
238
+ data = await response.json()
239
+ except aiohttp.ClientResponseError as e:
240
+ raise KlokkuDataParsingError(f"Failed to parse JSON response: {e}")
241
+
242
+ try:
243
+ result = [User(id=user["id"], username=user["username"], display_name=user["displayName"]) for user in data]
244
+ except (KeyError, TypeError) as e:
245
+ raise KlokkuDataStructureError(f"Unexpected data structure in response: {e}")
246
+ except aiohttp.ClientConnectionError as e:
247
+ raise KlokkuNetworkError(f"Connection error: {e}")
248
+
249
+ # Close the session if we created it in this method
250
+ if close_after:
251
+ await self.session.close()
252
+ self.session = None
253
+
254
+ return result
255
+ except KlokkuApiError as e:
256
+ _LOGGER.error(f"Error fetching all users: {e}")
257
+ return None
258
+
259
+ async def set_current_budget(self, budget_id: int):
260
+ """
261
+ Sets the currently used budget.
262
+ :param budget_id: The ID of the budget to set as current.
263
+ :return: The response data or None if an error occurred.
264
+ :raises KlokkuAuthenticationError: If the user is not authenticated.
265
+ :raises KlokkuNetworkError: If there's a network error.
266
+ :raises KlokkuApiResponseError: If the API returns an error response.
267
+ :raises KlokkuDataParsingError: If there's an error when parsing the response.
268
+ """
269
+ if not self.user_id:
270
+ error = KlokkuAuthenticationError("Unauthenticated - cannot set current budget")
271
+ _LOGGER.warning(str(error))
272
+ return None
273
+
274
+ url = f"{self.url}api/event"
275
+ try:
276
+ # Create a session if one doesn't exist
277
+ if not self.session:
278
+ self.session = aiohttp.ClientSession()
279
+ close_after = True
280
+ else:
281
+ close_after = False
282
+
283
+ try:
284
+ async with self.session.post(url, headers=self.__headers(self.user_id), json={"budgetId": budget_id}) as response:
285
+ if response.status >= 400:
286
+ error_msg = await response.text()
287
+ raise KlokkuApiResponseError(response.status, error_msg)
288
+
289
+ try:
290
+ data = await response.json()
291
+ except aiohttp.ClientResponseError as e:
292
+ raise KlokkuDataParsingError(f"Failed to parse JSON response: {e}")
293
+ except aiohttp.ClientConnectionError as e:
294
+ raise KlokkuNetworkError(f"Connection error: {e}")
295
+
296
+ # Close the session if we created it in this method
297
+ if close_after:
298
+ await self.session.close()
299
+ self.session = None
300
+
301
+ return data
302
+ except KlokkuApiError as e:
303
+ _LOGGER.error(f"Error setting current budget: {e}")
304
+ return None
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.3
2
+ Name: klokku-python-client
3
+ Version: 0.1.0
4
+ Summary: Klokku REST API client
5
+ License: MIT
6
+ Author: Mariusz Józala
7
+ Requires-Python: >=3.13
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Requires-Dist: aiohttp (>=3.11.16,<4.0.0)
11
+ Project-URL: Homepage, https://klokku.com
12
+ Project-URL: Issues, https://github.com/klokku/klokku-python-client/issues
13
+ Project-URL: Repository, https://github.com/klokku/klokku-python-client
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Klokku Python Client
17
+
18
+ A Python client for interacting with the Klokku REST API.
19
+
20
+ ## Installation
21
+
22
+ ### Using pip
23
+
24
+ ```bash
25
+ pip install klokku-python-client
26
+ ```
27
+
28
+ ### Using Poetry
29
+
30
+ ```bash
31
+ poetry add klokku-python-client
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ The client provides an asynchronous interface to the Klokku API:
37
+
38
+ ```python
39
+ import asyncio
40
+ from klokku_python_client import KlokkuApi
41
+
42
+ async def main():
43
+ # Create a client instance
44
+ async with KlokkuApi("https://api.klokku.example.com/") as client:
45
+ # Authenticate with a username
46
+ authenticated = await client.authenticate("your_username")
47
+ if not authenticated:
48
+ print("Authentication failed")
49
+ return
50
+
51
+ # Get all budgets
52
+ budgets = await client.get_all_budgets()
53
+ if budgets:
54
+ print(f"Found {len(budgets)} budgets:")
55
+ for budget in budgets:
56
+ print(f"- {budget.name} (ID: {budget.id})")
57
+
58
+ # Get current event
59
+ current_event = await client.get_current_event()
60
+ if current_event:
61
+ print(f"Current budget: {current_event.budget.name}")
62
+ print(f"Started at: {current_event.startTime}")
63
+
64
+ # Set a different budget
65
+ if budgets and len(budgets) > 1:
66
+ new_budget_id = budgets[1].id
67
+ result = await client.set_current_budget(new_budget_id)
68
+ if result:
69
+ print(f"Set current budget to ID: {new_budget_id}")
70
+
71
+ # Run the async function
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ ## Features
76
+
77
+ - Asynchronous API client using `aiohttp`
78
+ - Authentication with username
79
+ - Get list of users
80
+ - Get all budgets
81
+ - Get current event/budget
82
+ - Set current budget
83
+ - Context manager support for proper resource cleanup
84
+
85
+ ## Development
86
+
87
+ ### Setup
88
+
89
+ 1. Clone the repository
90
+ 2. Install dependencies with Poetry:
91
+
92
+ ```bash
93
+ poetry install
94
+ ```
95
+
96
+ ### Testing
97
+
98
+ The project uses pytest for testing with aioresponses for mocking HTTP requests.
99
+
100
+ To install test dependencies:
101
+
102
+ ```bash
103
+ pip install -e ".[test]"
104
+ ```
105
+
106
+ Or with Poetry:
107
+
108
+ ```bash
109
+ poetry install --with test
110
+ ```
111
+
112
+ To run the tests:
113
+
114
+ ```bash
115
+ pytest
116
+ ```
117
+
118
+ For more details about testing, see the [tests README](tests/README.md).
119
+
120
+ ## Requirements
121
+
122
+ - Python 3.13+
123
+ - aiohttp 3.11+
124
+
125
+ ## License
126
+
127
+ [MIT](LICENSE)
128
+
@@ -0,0 +1,5 @@
1
+ klokku_python_client/__init__.py,sha256=1AOx109cSpBOiz_-bLMFgZxEU-EkYl9KksZL3aK3pYM,631
2
+ klokku_python_client/api_client.py,sha256=6xXKOvlTuE57ynSpN70ZSRt2w5-E3yyLMg94yW7mzeI,11735
3
+ klokku_python_client-0.1.0.dist-info/METADATA,sha256=pZXy4QbfegI_A02G7A6EKOWNinNPA8D-n6qGGaEQe6c,2767
4
+ klokku_python_client-0.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
5
+ klokku_python_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.1.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any