PyHiveLMS 5.12.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.
- pyhive/__init__.py +13 -0
- pyhive/client.py +355 -0
- pyhive/src/__init__.py +0 -0
- pyhive/src/authenticated_hive_client.py +250 -0
- pyhive/src/types/__init__.py +0 -0
- pyhive/src/types/assignment.py +192 -0
- pyhive/src/types/class_.py +113 -0
- pyhive/src/types/common.py +56 -0
- pyhive/src/types/core_item.py +22 -0
- pyhive/src/types/enums/__init__.py +0 -0
- pyhive/src/types/enums/action_enum.py +18 -0
- pyhive/src/types/enums/assignment_status_enum.py +17 -0
- pyhive/src/types/enums/class_type_enum.py +13 -0
- pyhive/src/types/enums/clearance_enum.py +16 -0
- pyhive/src/types/enums/event_type_enum.py +14 -0
- pyhive/src/types/enums/exercise_patbas_enum.py +15 -0
- pyhive/src/types/enums/exercise_preview_types.py +15 -0
- pyhive/src/types/enums/form_field_type_enum.py +15 -0
- pyhive/src/types/enums/gender_enum.py +14 -0
- pyhive/src/types/enums/help_response_type_enum.py +14 -0
- pyhive/src/types/enums/help_status_enum.py +13 -0
- pyhive/src/types/enums/help_type_enum.py +18 -0
- pyhive/src/types/enums/queue_rule_enum.py +15 -0
- pyhive/src/types/enums/status_enum.py +21 -0
- pyhive/src/types/enums/sync_status_enum.py +15 -0
- pyhive/src/types/enums/visibility_enum.py +14 -0
- pyhive/src/types/event.py +140 -0
- pyhive/src/types/event_attendees_type_0_item.py +69 -0
- pyhive/src/types/event_color.py +63 -0
- pyhive/src/types/exercise.py +192 -0
- pyhive/src/types/form_field.py +149 -0
- pyhive/src/types/help_.py +275 -0
- pyhive/src/types/help_response.py +113 -0
- pyhive/src/types/help_response_segel_nested.py +129 -0
- pyhive/src/types/module.py +107 -0
- pyhive/src/types/notification_nested.py +80 -0
- pyhive/src/types/program.py +172 -0
- pyhive/src/types/queue.py +150 -0
- pyhive/src/types/queue_item.py +88 -0
- pyhive/src/types/subject.py +116 -0
- pyhive/src/types/tag.py +62 -0
- pyhive/src/types/user.py +375 -0
- pyhivelms-5.12.0.dist-info/METADATA +92 -0
- pyhivelms-5.12.0.dist-info/RECORD +45 -0
- pyhivelms-5.12.0.dist-info/WHEEL +4 -0
pyhive/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Public package for the pyhive distribution.
|
|
2
|
+
|
|
3
|
+
Expose the public convenience symbol `HiveClient` at package level so users
|
|
4
|
+
can do `from pyhive import HiveClient`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# Import the implementation from the `pyhive` package (implementation
|
|
10
|
+
# lives there) and expose the client at package level.
|
|
11
|
+
from pyhive.client import HiveClient # re-export
|
|
12
|
+
|
|
13
|
+
__all__ = ["HiveClient"]
|
pyhive/client.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""High-level Hive API client.
|
|
2
|
+
|
|
3
|
+
This module provides ``HiveClient``, a small, synchronous authenticated
|
|
4
|
+
client for the Hive service. It exposes convenience methods that return
|
|
5
|
+
typed model objects from :mod:`src.types` and generator-based list
|
|
6
|
+
endpoints for memory-efficient iteration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from typing import TYPE_CHECKING, Optional, TypeVar, Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from src.authenticated_hive_client import _AuthenticatedHiveClient
|
|
15
|
+
from src.types.class_ import Class
|
|
16
|
+
from src.types.enums.class_type_enum import ClassTypeEnum
|
|
17
|
+
from src.types.exercise import Exercise
|
|
18
|
+
from src.types.form_field import FormField
|
|
19
|
+
from src.types.module import Module
|
|
20
|
+
from src.types.program import Program
|
|
21
|
+
from src.types.subject import Subject
|
|
22
|
+
from src.types.user import User
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from src.types.core_item import HiveCoreItem
|
|
26
|
+
|
|
27
|
+
CoreItemTypeT = TypeVar("CoreItemTypeT", bound="HiveCoreItem")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class HiveClient(_AuthenticatedHiveClient):
|
|
31
|
+
"""HTTP client for accessing Hive API.
|
|
32
|
+
|
|
33
|
+
The client is used as a context manager and provides typed helpers for
|
|
34
|
+
common Hive resources (programs, subjects, modules, exercises, users,
|
|
35
|
+
classes, and form fields).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __enter__(self) -> "HiveClient":
|
|
39
|
+
"""Enter context manager and return this client instance.
|
|
40
|
+
|
|
41
|
+
This delegates to the base class context manager which manages the
|
|
42
|
+
underlying :class:`httpx.Client` session.
|
|
43
|
+
"""
|
|
44
|
+
super().__enter__()
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def get_course_programs(
|
|
48
|
+
self,
|
|
49
|
+
id__in: Optional[list[int]] = None,
|
|
50
|
+
) -> Generator[Program]:
|
|
51
|
+
"""Yield :class:`Program` objects.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
id__in: Optional list of program ids to filter the results.
|
|
55
|
+
|
|
56
|
+
Yields:
|
|
57
|
+
Program instances parsed from the API response.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
query_params = httpx.QueryParams()
|
|
61
|
+
if id__in is not None:
|
|
62
|
+
query_params.set("id__in", id__in)
|
|
63
|
+
return (
|
|
64
|
+
Program.from_dict(program_dict, hive_client=self)
|
|
65
|
+
for program_dict in super().get(
|
|
66
|
+
"/api/core/course/programs/",
|
|
67
|
+
params=query_params,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_program(self, program_id: int) -> Program:
|
|
72
|
+
"""Return a single :class:`Program` by id.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
program_id: The program identifier.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A populated :class:`Program` object.
|
|
79
|
+
"""
|
|
80
|
+
return Program.from_dict(
|
|
81
|
+
super().get(f"/api/core/course/programs/{program_id}/"),
|
|
82
|
+
hive_client=self,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def get_course_subjects(
|
|
86
|
+
self,
|
|
87
|
+
parent_program__id__in: Optional[list[int]] = None,
|
|
88
|
+
) -> Generator[Subject]:
|
|
89
|
+
"""Yield :class:`Subject` objects for course subjects.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
parent_program__id__in: Optional list of parent program ids to
|
|
93
|
+
filter subjects.
|
|
94
|
+
|
|
95
|
+
Yields:
|
|
96
|
+
Subject instances.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
query_params = httpx.QueryParams()
|
|
100
|
+
if parent_program__id__in is not None:
|
|
101
|
+
query_params.set("parent_program__id__in", parent_program__id__in)
|
|
102
|
+
return (
|
|
103
|
+
Subject.from_dict(subject_dict, hive_client=self)
|
|
104
|
+
for subject_dict in super().get(
|
|
105
|
+
"/api/core/course/subjects/",
|
|
106
|
+
params=query_params,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def get_subject(self, subject_id: int) -> Subject:
|
|
111
|
+
"""Return a single :class:`Subject` by id.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
subject_id: The subject identifier.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A populated :class:`Subject` object.
|
|
118
|
+
"""
|
|
119
|
+
return Subject.from_dict(
|
|
120
|
+
super().get(f"/api/core/course/subjects/{subject_id}/"),
|
|
121
|
+
hive_client=self,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_course_modules(
|
|
125
|
+
self,
|
|
126
|
+
/,
|
|
127
|
+
parent_subject__id: Optional[int] = None,
|
|
128
|
+
parent_subject__parent_program__id__in: Optional[list[int]] = None,
|
|
129
|
+
) -> Generator[Module]:
|
|
130
|
+
"""Yield :class:`Module` objects for course modules.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
parent_subject__id: Optional subject id to restrict modules.
|
|
134
|
+
parent_subject__parent_program__id__in: Optional list of program
|
|
135
|
+
ids to restrict modules.
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
Module instances.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
query_params = httpx.QueryParams()
|
|
142
|
+
if parent_subject__parent_program__id__in is not None:
|
|
143
|
+
query_params.set(
|
|
144
|
+
"parent_subject__parent_program__id__in",
|
|
145
|
+
parent_subject__parent_program__id__in,
|
|
146
|
+
)
|
|
147
|
+
if parent_subject__id is not None:
|
|
148
|
+
query_params.set("parent_subject__id", parent_subject__id)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
Module.from_dict(subject_dict, hive_client=self)
|
|
152
|
+
for subject_dict in super().get(
|
|
153
|
+
"/api/core/course/modules/",
|
|
154
|
+
params=query_params,
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def get_module(self, module_id: int) -> Module:
|
|
159
|
+
"""Return a single :class:`Module` by id.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
module_id: The module identifier.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
A populated :class:`Module` object.
|
|
166
|
+
"""
|
|
167
|
+
return Module.from_dict(
|
|
168
|
+
super().get(f"/api/core/course/modules/{module_id}/"),
|
|
169
|
+
hive_client=self,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def get_exercises( # pylint: disable=too-many-arguments
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
parent_module__id: Optional[int] = None,
|
|
176
|
+
parent_module__parent_subject__id: Optional[int] = None,
|
|
177
|
+
parent_module__parent_subject__parent_program__id__in: Optional[
|
|
178
|
+
list[int]
|
|
179
|
+
] = None,
|
|
180
|
+
queue__id: Optional[int] = None,
|
|
181
|
+
tags__id__in: Optional[list[int]] = None,
|
|
182
|
+
):
|
|
183
|
+
"""Yield :class:`Exercise` objects.
|
|
184
|
+
|
|
185
|
+
Accepts common filtering keyword args which are forwarded to the
|
|
186
|
+
underlying list endpoint.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
return self._get_core_items(
|
|
190
|
+
"/api/core/course/exercises/",
|
|
191
|
+
Exercise,
|
|
192
|
+
parent_module__id=parent_module__id,
|
|
193
|
+
parent_module__parent_subject__id=parent_module__parent_subject__id,
|
|
194
|
+
parent_module__parent_subject__parent_program__id__in=parent_module__parent_subject__parent_program__id__in,
|
|
195
|
+
queue__id=queue__id,
|
|
196
|
+
tags__id__in=tags__id__in,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def get_exercise(self, exercise_id: int) -> Exercise:
|
|
200
|
+
"""Return a single :class:`Exercise` by id.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
exercise_id: The exercise identifier.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
A populated :class:`Exercise` object.
|
|
207
|
+
"""
|
|
208
|
+
return Exercise.from_dict(
|
|
209
|
+
super().get(f"/api/core/course/exercises/{exercise_id}/"),
|
|
210
|
+
hive_client=self,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def get_users( # pylint: disable=too-many-arguments
|
|
214
|
+
self,
|
|
215
|
+
*,
|
|
216
|
+
classes__id__in: Optional[list[int]] = None,
|
|
217
|
+
clearance__in: Optional[list[int]] = None,
|
|
218
|
+
id__in: Optional[list[int]] = None,
|
|
219
|
+
mentor__id: Optional[int] = None,
|
|
220
|
+
mentor__id__in: Optional[list[int]] = None,
|
|
221
|
+
program__id__in: Optional[list[int]] = None,
|
|
222
|
+
program_checker__id__in: Optional[list[int]] = None,
|
|
223
|
+
) -> Generator[User]:
|
|
224
|
+
"""Yield :class:`User` objects from the management users endpoint.
|
|
225
|
+
|
|
226
|
+
All kwargs are optional filters forwarded to the API.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
return self._get_core_items(
|
|
230
|
+
"/api/core/management/users/",
|
|
231
|
+
User,
|
|
232
|
+
classes__id__in=classes__id__in,
|
|
233
|
+
clearance__in=clearance__in,
|
|
234
|
+
id__in=id__in,
|
|
235
|
+
mentor__id=mentor__id,
|
|
236
|
+
mentor__id__in=mentor__id__in,
|
|
237
|
+
program__id__in=program__id__in,
|
|
238
|
+
program_checker__id__in=program_checker__id__in,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def get_user(self, user_id: int) -> User:
|
|
242
|
+
"""Return a single :class:`User` by id.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
user_id: The user identifier.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
A populated :class:`User` object.
|
|
249
|
+
"""
|
|
250
|
+
return User.from_dict(
|
|
251
|
+
super().get(f"/api/core/management/users/{user_id}/"),
|
|
252
|
+
hive_client=self,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def get_classes(
|
|
256
|
+
self,
|
|
257
|
+
*,
|
|
258
|
+
id__in: Optional[list[int]] = None,
|
|
259
|
+
name: Optional[str] = None,
|
|
260
|
+
program__id__in: Optional[list[int]] = None,
|
|
261
|
+
type_: Optional[ClassTypeEnum] = None,
|
|
262
|
+
) -> Generator[Class]:
|
|
263
|
+
"""Yield :class:`Class` objects from the management classes endpoint.
|
|
264
|
+
|
|
265
|
+
Filters may be provided as keyword arguments.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
return self._get_core_items(
|
|
269
|
+
"/api/core/management/classes/",
|
|
270
|
+
Class,
|
|
271
|
+
id__in=id__in,
|
|
272
|
+
name=name,
|
|
273
|
+
program__id__in=program__id__in,
|
|
274
|
+
type_=type_,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def get_class(
|
|
278
|
+
self,
|
|
279
|
+
class_id: int,
|
|
280
|
+
) -> Class:
|
|
281
|
+
"""Return a single :class:`Class` by id.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
class_id: The class identifier.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
A populated :class:`Class` object.
|
|
288
|
+
"""
|
|
289
|
+
return Class.from_dict(
|
|
290
|
+
super().get(f"/api/core/management/classes/{class_id}/"),
|
|
291
|
+
hive_client=self,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def get_exercise_fields(
|
|
295
|
+
self,
|
|
296
|
+
exercise_id: int,
|
|
297
|
+
) -> Generator[FormField]:
|
|
298
|
+
"""Yield :class:`FormField` objects for an exercise.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
exercise_id: The exercise identifier.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
return self._get_core_items(
|
|
305
|
+
f"/api/core/course/exercises/{exercise_id}/fields/",
|
|
306
|
+
FormField,
|
|
307
|
+
exercise_id=exercise_id,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def get_exercise_field(
|
|
311
|
+
self,
|
|
312
|
+
exercise_id: int,
|
|
313
|
+
field_id: int,
|
|
314
|
+
) -> FormField:
|
|
315
|
+
"""Return a single :class:`FormField` for an exercise by id.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
exercise_id: The exercise identifier.
|
|
319
|
+
field_id: The field identifier.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
A populated :class:`FormField` object.
|
|
323
|
+
"""
|
|
324
|
+
return FormField.from_dict(
|
|
325
|
+
super().get(f"/api/core/course/exercises/{exercise_id}/fields/{field_id}/"),
|
|
326
|
+
hive_client=self,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _get_core_items(
|
|
330
|
+
self,
|
|
331
|
+
endpoint: str,
|
|
332
|
+
item_type: type[CoreItemTypeT],
|
|
333
|
+
/,
|
|
334
|
+
**kwargs: dict[str, Any], # noqa: ANN401
|
|
335
|
+
) -> Generator[CoreItemTypeT]:
|
|
336
|
+
"""Internal helper to yield typed core items from a list endpoint.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
endpoint: API endpoint path for the list resource.
|
|
340
|
+
item_type: Model class with a ``from_dict`` constructor.
|
|
341
|
+
**kwargs: Filter query parameters forwarded to the endpoint.
|
|
342
|
+
|
|
343
|
+
Yields:
|
|
344
|
+
Instances of ``item_type`` created via ``from_dict``.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
query_params = httpx.QueryParams()
|
|
348
|
+
for name, value in kwargs.items():
|
|
349
|
+
if value is not None:
|
|
350
|
+
query_params = query_params.set(name, value)
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
item_type.from_dict(x, hive_client=self)
|
|
354
|
+
for x in super().get(endpoint, params=query_params)
|
|
355
|
+
)
|
pyhive/src/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Authentication helpers and a small authenticated HTTP client for Hive.
|
|
2
|
+
|
|
3
|
+
This module provides decorators that add retry and token-refresh behavior to
|
|
4
|
+
HTTP calls and an internal ``_AuthenticatedHiveClient`` which wraps an
|
|
5
|
+
:class:`httpx.Client` and handles login/refresh for the Hive API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Any, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
F = TypeVar("F", bound=Callable[..., httpx.Response])
|
|
17
|
+
|
|
18
|
+
MAX_RETRIES_ON_SERVER_ERRORS = 5
|
|
19
|
+
INITIAL_BACKOFF_SECONDS = 0.5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _retry_on_bad_gateway(func: F) -> F:
|
|
23
|
+
"""Decorator: retry a request when the server returns HTTP 502.
|
|
24
|
+
|
|
25
|
+
The wrapped function is expected to return an :class:`httpx.Response`.
|
|
26
|
+
Retries use exponential backoff and will re-raise the final response's
|
|
27
|
+
HTTP error if all retries fail.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@functools.wraps(func)
|
|
31
|
+
def wrapper(self: "_AuthenticatedHiveClient", *args: Any, **kwargs: Any):
|
|
32
|
+
delay = INITIAL_BACKOFF_SECONDS
|
|
33
|
+
if MAX_RETRIES_ON_SERVER_ERRORS <= 0:
|
|
34
|
+
raise ValueError("MAX_RETRIES_ON_SERVER_ERRORS must be greater than 0")
|
|
35
|
+
response = None
|
|
36
|
+
for attempt in range(MAX_RETRIES_ON_SERVER_ERRORS):
|
|
37
|
+
response = func(self, *args, **kwargs)
|
|
38
|
+
if response.status_code != httpx.codes.BAD_GATEWAY.value:
|
|
39
|
+
return response
|
|
40
|
+
if attempt < MAX_RETRIES_ON_SERVER_ERRORS - 1:
|
|
41
|
+
time.sleep(delay)
|
|
42
|
+
delay *= 2
|
|
43
|
+
assert response is not None
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response
|
|
46
|
+
|
|
47
|
+
return cast("F", wrapper)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _refresh_token_on_unauthorized(func: F) -> F:
|
|
51
|
+
"""Decorator: refresh access token and retry on HTTP 401 Unauthorized.
|
|
52
|
+
|
|
53
|
+
If the wrapped function returns a 401 status, the client's
|
|
54
|
+
``_refresh_access_token`` is called and the request is retried once.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@functools.wraps(func)
|
|
58
|
+
def wrapper(self: "_AuthenticatedHiveClient", *args: Any, **kwargs: Any):
|
|
59
|
+
response = func(self, *args, **kwargs)
|
|
60
|
+
if response.status_code == httpx.codes.UNAUTHORIZED.value:
|
|
61
|
+
self._refresh_access_token() # pylint: disable=protected-access
|
|
62
|
+
response = func(self, *args, **kwargs)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
return response
|
|
65
|
+
|
|
66
|
+
return cast("F", wrapper)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _with_retries_and_token_refresh(func: F) -> F:
|
|
70
|
+
"""Compose the retry and token-refresh decorators.
|
|
71
|
+
|
|
72
|
+
Use this to wrap HTTP methods so they automatically handle transient
|
|
73
|
+
502 errors and expired access tokens.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
return _refresh_token_on_unauthorized(_retry_on_bad_gateway(func))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _AuthenticatedHiveClient:
|
|
80
|
+
"""Internal class used to handle authentication and re-authentication with Hive web endpoint."""
|
|
81
|
+
|
|
82
|
+
_refresh_token: str
|
|
83
|
+
_access_token: str
|
|
84
|
+
_session: httpx.Client
|
|
85
|
+
username: str
|
|
86
|
+
|
|
87
|
+
def __init__( # pylint: disable=too-many-arguments
|
|
88
|
+
self,
|
|
89
|
+
username: str,
|
|
90
|
+
password: str,
|
|
91
|
+
hive_url: str,
|
|
92
|
+
*,
|
|
93
|
+
timeout: httpx.Timeout | float | None = None,
|
|
94
|
+
headers: dict[str, str] | None = None,
|
|
95
|
+
verify: bool | str | None = None,
|
|
96
|
+
**kwargs: Any,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Create an authenticated client.
|
|
99
|
+
|
|
100
|
+
Common HTTP client options may be provided explicitly (typed) or via
|
|
101
|
+
``**kwargs`` and will be forwarded to :class:`httpx.Client`.
|
|
102
|
+
|
|
103
|
+
Typed kwargs provided (timeout, headers, verify) take precedence; the
|
|
104
|
+
rest are forwarded from ``kwargs``.
|
|
105
|
+
"""
|
|
106
|
+
self.username = username
|
|
107
|
+
self.hive_url = hive_url
|
|
108
|
+
|
|
109
|
+
client_kwargs: dict[str, Any] = {}
|
|
110
|
+
if timeout is not None:
|
|
111
|
+
client_kwargs["timeout"] = timeout
|
|
112
|
+
if headers is not None:
|
|
113
|
+
client_kwargs["headers"] = headers
|
|
114
|
+
if verify is not None:
|
|
115
|
+
client_kwargs["verify"] = verify
|
|
116
|
+
|
|
117
|
+
# Include any other httpx.Client kwargs passed in **kwargs
|
|
118
|
+
client_kwargs.update(kwargs)
|
|
119
|
+
|
|
120
|
+
self._session = httpx.Client(
|
|
121
|
+
base_url=hive_url,
|
|
122
|
+
**client_kwargs,
|
|
123
|
+
).__enter__()
|
|
124
|
+
self._login(username, password)
|
|
125
|
+
|
|
126
|
+
def __enter__(self) -> "_AuthenticatedHiveClient":
|
|
127
|
+
"""Enter context manager and return this client instance.
|
|
128
|
+
|
|
129
|
+
The underlying :class:`httpx.Client` is managed by this object's
|
|
130
|
+
lifecycle; entering the context returns the authenticated client so
|
|
131
|
+
callers can perform API calls.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(
|
|
137
|
+
self,
|
|
138
|
+
type_: type[BaseException] | None,
|
|
139
|
+
value: BaseException | None,
|
|
140
|
+
traceback: TracebackType | None,
|
|
141
|
+
) -> bool | None:
|
|
142
|
+
"""Exit the context and close the underlying httpx session.
|
|
143
|
+
|
|
144
|
+
This delegates to the managed :class:`httpx.Client`'s ``__exit__``
|
|
145
|
+
method to ensure resources are released.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
self._session.__exit__(type_, value, traceback)
|
|
149
|
+
|
|
150
|
+
def _login(self, username: str, password: str) -> None:
|
|
151
|
+
"""Perform an authentication request and store access/refresh tokens.
|
|
152
|
+
|
|
153
|
+
This sets the ``Authorization`` header on the underlying session.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
response = self._session.post(
|
|
157
|
+
"/api/core/token/",
|
|
158
|
+
json={"username": username, "password": password},
|
|
159
|
+
)
|
|
160
|
+
response.raise_for_status()
|
|
161
|
+
data = response.json()
|
|
162
|
+
self._access_token = data["access"]
|
|
163
|
+
self._refresh_token = data["refresh"]
|
|
164
|
+
|
|
165
|
+
self._session.headers.update({"Authorization": f"Bearer {self._access_token}"})
|
|
166
|
+
|
|
167
|
+
def _refresh_access_token(self) -> None:
|
|
168
|
+
"""Refresh the access token using the stored refresh token.
|
|
169
|
+
|
|
170
|
+
Updates the stored access and refresh tokens and the session header.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
response = self._session.post(
|
|
174
|
+
"/api/core/token/refresh/",
|
|
175
|
+
json={"refresh": self._refresh_token},
|
|
176
|
+
)
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
data = response.json()
|
|
179
|
+
self._access_token = data["access"]
|
|
180
|
+
self._refresh_token = data["refresh"]
|
|
181
|
+
|
|
182
|
+
self._session.headers.update({"Authorization": f"Bearer {self._access_token}"})
|
|
183
|
+
|
|
184
|
+
@_with_retries_and_token_refresh
|
|
185
|
+
def _get(
|
|
186
|
+
self, endpoint: str, params: httpx.QueryParams | None = None
|
|
187
|
+
) -> httpx.Response:
|
|
188
|
+
"""Low-level GET that returns an :class:`httpx.Response`.
|
|
189
|
+
|
|
190
|
+
This is decorated to handle retries and token refresh automatically.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
return self._session.get(endpoint, params=params)
|
|
194
|
+
|
|
195
|
+
@_with_retries_and_token_refresh
|
|
196
|
+
def _post(self, endpoint: str, data: dict[Any, Any]) -> httpx.Response:
|
|
197
|
+
"""Low-level POST that returns an :class:`httpx.Response` with JSON body.
|
|
198
|
+
|
|
199
|
+
The ``data`` is JSON-encoded into the request body.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
return self._session.post(endpoint, json=data)
|
|
203
|
+
|
|
204
|
+
@_with_retries_and_token_refresh
|
|
205
|
+
def _patch(self, endpoint: str, data: dict[Any, Any]) -> httpx.Response:
|
|
206
|
+
"""Low-level PATCH request; returns :class:`httpx.Response`.
|
|
207
|
+
|
|
208
|
+
The ``data`` is JSON-encoded into the request body.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
return self._session.patch(endpoint, json=data)
|
|
212
|
+
|
|
213
|
+
@_with_retries_and_token_refresh
|
|
214
|
+
def _delete(self, endpoint: str) -> httpx.Response:
|
|
215
|
+
"""Low-level DELETE request; returns :class:`httpx.Response`."""
|
|
216
|
+
|
|
217
|
+
return self._session.delete(endpoint)
|
|
218
|
+
|
|
219
|
+
@_with_retries_and_token_refresh
|
|
220
|
+
def _put(self, endpoint: str, data: dict[Any, Any]) -> httpx.Response:
|
|
221
|
+
"""Low-level PUT request; returns :class:`httpx.Response`.
|
|
222
|
+
|
|
223
|
+
The ``data`` is JSON-encoded into the request body.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
return self._session.put(endpoint, json=data)
|
|
227
|
+
|
|
228
|
+
def get(self, endpoint: str, params: httpx.QueryParams | None = None) -> Any:
|
|
229
|
+
"""High-level GET that returns parsed JSON from the response.
|
|
230
|
+
|
|
231
|
+
This calls the decorated ``_get`` helper and returns its JSON body.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
return self._get(endpoint, params).json()
|
|
235
|
+
|
|
236
|
+
def post(self, endpoint: str, data: dict[Any, Any]) -> Any:
|
|
237
|
+
"""High-level POST that returns parsed JSON from the response.
|
|
238
|
+
|
|
239
|
+
The ``data`` dict is JSON-encoded for the request body.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
return self._post(endpoint, data).json()
|
|
243
|
+
|
|
244
|
+
def __repr__(self) -> str:
|
|
245
|
+
"""Return a short representation including username and hive_url.
|
|
246
|
+
|
|
247
|
+
The representation intentionally omits secrets.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
return f"HiveClient({self.username!r}, input(), {self.hive_url!r})"
|
|
File without changes
|