PyHiveLMS 1.0.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/cli/__init__.py +0 -0
- pyhive/cli/main.py +30 -0
- pyhive/client.py +570 -0
- pyhive/src/__init__.py +0 -0
- pyhive/src/_generated_versions.py +3 -0
- pyhive/src/api_versions.py +6 -0
- pyhive/src/authenticated_hive_client.py +250 -0
- pyhive/src/types/__init__.py +0 -0
- pyhive/src/types/assignment.py +210 -0
- pyhive/src/types/assignment_response.py +205 -0
- pyhive/src/types/assignment_response_content.py +131 -0
- pyhive/src/types/autocheck_status.py +94 -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_response_type_enum.py +17 -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 +216 -0
- pyhive/src/types/form_field.py +152 -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 +141 -0
- pyhive/src/types/notification_nested.py +80 -0
- pyhive/src/types/program.py +180 -0
- pyhive/src/types/queue.py +150 -0
- pyhive/src/types/queue_item.py +88 -0
- pyhive/src/types/subject.py +156 -0
- pyhive/src/types/tag.py +62 -0
- pyhive/src/types/user.py +450 -0
- pyhive/types.py +23 -0
- pyhivelms-1.0.0.dist-info/METADATA +156 -0
- pyhivelms-1.0.0.dist-info/RECORD +55 -0
- pyhivelms-1.0.0.dist-info/WHEEL +4 -0
- pyhivelms-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -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, headers={"Accept": "application/json"})
|
|
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
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Defines the Assignment type and related logic for representing student assignments in the Hive API."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Self, TypeVar, cast, Generator
|
|
6
|
+
|
|
7
|
+
from attrs import define, field
|
|
8
|
+
from dateutil.parser import isoparse
|
|
9
|
+
from .common import UNSET, Unset
|
|
10
|
+
from .core_item import HiveCoreItem
|
|
11
|
+
from .enums.assignment_status_enum import AssignmentStatusEnum
|
|
12
|
+
from .notification_nested import NotificationNested
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ...client import HiveClient
|
|
16
|
+
from .exercise import Exercise
|
|
17
|
+
from .user import User
|
|
18
|
+
from .assignment_response import AssignmentResponse
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T", bound="Assignment")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@define
|
|
24
|
+
class Assignment(HiveCoreItem):
|
|
25
|
+
"""Represents a student's assignment for an exercise.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
hive_client: Reference to the Hive API client.
|
|
29
|
+
id: Unique assignment ID.
|
|
30
|
+
user_id: ID of the assigned student.
|
|
31
|
+
checker_id: ID of the assigned checker, or None.
|
|
32
|
+
checker_first_name: First name of the checker.
|
|
33
|
+
checker_last_name: Last name of the checker.
|
|
34
|
+
is_subscribed: Whether the student is subscribed to updates.
|
|
35
|
+
exercise_id: ID of the exercise.
|
|
36
|
+
assignment_status: Current state of the assignment.
|
|
37
|
+
patbas: Whether it's a PATBAS assignment.
|
|
38
|
+
notifications: List of related notifications.
|
|
39
|
+
last_staff_updated: Timestamp of the last staff update.
|
|
40
|
+
work_time: Total work time in minutes.
|
|
41
|
+
student_assignment_status: The student's view of the assignment status.
|
|
42
|
+
description: Optional text description.
|
|
43
|
+
submission_count: Total number of submissions.
|
|
44
|
+
total_check_count: Number of total checks.
|
|
45
|
+
manual_check_count: Number of manual checks.
|
|
46
|
+
flagged: Whether the assignment is flagged for review.
|
|
47
|
+
timer: Optional timer state string.
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
hive_client: "HiveClient"
|
|
52
|
+
id: int
|
|
53
|
+
user_id: int
|
|
54
|
+
checker_id: None | int
|
|
55
|
+
checker_first_name: str
|
|
56
|
+
checker_last_name: str
|
|
57
|
+
is_subscribed: bool
|
|
58
|
+
exercise_id: int
|
|
59
|
+
assignment_status: AssignmentStatusEnum
|
|
60
|
+
patbas: bool
|
|
61
|
+
notifications: list["NotificationNested"]
|
|
62
|
+
last_staff_updated: datetime.datetime
|
|
63
|
+
work_time: int
|
|
64
|
+
student_assignment_status: Unset | AssignmentStatusEnum = UNSET
|
|
65
|
+
description: None | Unset | str = UNSET
|
|
66
|
+
submission_count: Unset | int = UNSET
|
|
67
|
+
total_check_count: Unset | int = UNSET
|
|
68
|
+
manual_check_count: Unset | int = UNSET
|
|
69
|
+
flagged: Unset | bool = UNSET
|
|
70
|
+
timer: None | Unset | str = UNSET
|
|
71
|
+
|
|
72
|
+
# Lazy-loaded objects
|
|
73
|
+
_user: "User | None" = field(init=False, default=None)
|
|
74
|
+
_checker: "User | None" = field(init=False, default=None)
|
|
75
|
+
_exercise: "Exercise | None" = field(init=False, default=None)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def user(self) -> "User":
|
|
79
|
+
"""Lazily load and return the user this assignment belongs to."""
|
|
80
|
+
if self._user is None:
|
|
81
|
+
self._user = self.hive_client.get_user(self.user_id)
|
|
82
|
+
return self._user
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def checker(self) -> "User | None":
|
|
86
|
+
"""Lazily load and return the checker (if any) assigned to this assignment."""
|
|
87
|
+
if self.checker_id is None:
|
|
88
|
+
return None
|
|
89
|
+
if self._checker is None:
|
|
90
|
+
self._checker = self.hive_client.get_user(self.checker_id)
|
|
91
|
+
return self._checker
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def exercise(self) -> "Exercise":
|
|
95
|
+
"""Lazily load and return the exercise associated with this assignment."""
|
|
96
|
+
if self._exercise is None:
|
|
97
|
+
self._exercise = self.hive_client.get_exercise(self.exercise_id)
|
|
98
|
+
return self._exercise
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict[str, Any]:
|
|
101
|
+
"""Serialize Assignment to a dictionary."""
|
|
102
|
+
result: dict[str, None | str | int | list[dict[str, Any]]] = {
|
|
103
|
+
"id": self.id,
|
|
104
|
+
"user": self.user_id,
|
|
105
|
+
"checker": self.checker_id,
|
|
106
|
+
"checker_first_name": self.checker_first_name,
|
|
107
|
+
"checker_last_name": self.checker_last_name,
|
|
108
|
+
"is_subscribed": self.is_subscribed,
|
|
109
|
+
"exercise": self.exercise_id,
|
|
110
|
+
"assignment_status": self.assignment_status.value,
|
|
111
|
+
"patbas": self.patbas,
|
|
112
|
+
"notifications": [n.to_dict() for n in self.notifications],
|
|
113
|
+
"last_staff_updated": self.last_staff_updated.isoformat(),
|
|
114
|
+
"work_time": self.work_time,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Conditionally include optional/unset fields
|
|
118
|
+
if not isinstance(self.student_assignment_status, Unset):
|
|
119
|
+
result["student_assignment_status"] = self.student_assignment_status.value
|
|
120
|
+
if not isinstance(self.description, Unset):
|
|
121
|
+
result["description"] = self.description
|
|
122
|
+
if not isinstance(self.submission_count, Unset):
|
|
123
|
+
result["submission_count"] = self.submission_count
|
|
124
|
+
if not isinstance(self.total_check_count, Unset):
|
|
125
|
+
result["total_check_count"] = self.total_check_count
|
|
126
|
+
if not isinstance(self.manual_check_count, Unset):
|
|
127
|
+
result["manual_check_count"] = self.manual_check_count
|
|
128
|
+
if not isinstance(self.flagged, Unset):
|
|
129
|
+
result["flagged"] = self.flagged
|
|
130
|
+
if not isinstance(self.timer, Unset):
|
|
131
|
+
result["timer"] = self.timer
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_dict(cls, src_dict: Mapping[str, Any], hive_client: "HiveClient") -> Self:
|
|
137
|
+
"""Deserialize Assignment from a dictionary."""
|
|
138
|
+
|
|
139
|
+
d = dict(src_dict)
|
|
140
|
+
|
|
141
|
+
notifications = [
|
|
142
|
+
NotificationNested.from_dict(n, hive_client=hive_client)
|
|
143
|
+
for n in d.pop("notifications", [])
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
student_assignment_status = (
|
|
147
|
+
AssignmentStatusEnum(d["student_assignment_status"])
|
|
148
|
+
if "student_assignment_status" in d
|
|
149
|
+
and not isinstance(d["student_assignment_status"], Unset)
|
|
150
|
+
else UNSET
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
description = d.pop("description", UNSET)
|
|
154
|
+
submission_count = d.pop("submission_count", UNSET)
|
|
155
|
+
total_check_count = d.pop("total_check_count", UNSET)
|
|
156
|
+
manual_check_count = d.pop("manual_check_count", UNSET)
|
|
157
|
+
flagged = d.pop("flagged", UNSET)
|
|
158
|
+
timer = d.pop("timer", UNSET)
|
|
159
|
+
|
|
160
|
+
return cls(
|
|
161
|
+
hive_client=hive_client,
|
|
162
|
+
id=d["id"],
|
|
163
|
+
user_id=d["user"],
|
|
164
|
+
checker_id=cast("int | None", d["checker"]),
|
|
165
|
+
checker_first_name=d["checker_first_name"],
|
|
166
|
+
checker_last_name=d["checker_last_name"],
|
|
167
|
+
is_subscribed=d["is_subscribed"],
|
|
168
|
+
exercise_id=d["exercise"],
|
|
169
|
+
assignment_status=AssignmentStatusEnum(d["assignment_status"]),
|
|
170
|
+
patbas=d["patbas"],
|
|
171
|
+
notifications=notifications,
|
|
172
|
+
last_staff_updated=isoparse(d["last_staff_updated"]),
|
|
173
|
+
work_time=d["work_time"],
|
|
174
|
+
student_assignment_status=student_assignment_status,
|
|
175
|
+
description=cast("str | None | Unset", description),
|
|
176
|
+
submission_count=cast("int | Unset", submission_count),
|
|
177
|
+
total_check_count=cast("int | Unset", total_check_count),
|
|
178
|
+
manual_check_count=cast("int | Unset", manual_check_count),
|
|
179
|
+
flagged=cast("bool | Unset", flagged),
|
|
180
|
+
timer=cast("str | None | Unset", timer),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def __eq__(self, value: object) -> bool:
|
|
184
|
+
if not isinstance(value, Assignment):
|
|
185
|
+
return False
|
|
186
|
+
return (
|
|
187
|
+
self.id == value.id
|
|
188
|
+
and self.exercise_id == value.exercise_id
|
|
189
|
+
and self.user_id == value.user_id
|
|
190
|
+
and self.checker_id == value.checker_id
|
|
191
|
+
and self.assignment_status == value.assignment_status
|
|
192
|
+
and self.exercise == value.exercise
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def __lt__(self, value: object) -> bool:
|
|
196
|
+
if not isinstance(value, Assignment):
|
|
197
|
+
return NotImplemented
|
|
198
|
+
return self.user.number < value.user.number
|
|
199
|
+
|
|
200
|
+
def get_responses(self) -> Generator["AssignmentResponse", None, None]:
|
|
201
|
+
"""Fetch all responses to this assignment.
|
|
202
|
+
Responses include both student and mentor submissions, comments, WIP, ..."""
|
|
203
|
+
return self.hive_client.get_assignment_responses(assignment=self.id)
|
|
204
|
+
|
|
205
|
+
def __iter__(self) -> Generator["Assignment", None, None]:
|
|
206
|
+
"""Allow iteration over this Assignment to yield its responses."""
|
|
207
|
+
yield from self.get_responses()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
AssignmentLike = TypeVar("AssignmentLike", Assignment, int)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Responses to assignments given to students."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypeVar, Union, Generator
|
|
5
|
+
from attrs import define, field
|
|
6
|
+
from dateutil.parser import isoparse
|
|
7
|
+
|
|
8
|
+
from .core_item import HiveCoreItem
|
|
9
|
+
from .autocheck_status import AutoCheckStatus
|
|
10
|
+
from .assignment import Assignment
|
|
11
|
+
from .enums.assignment_response_type_enum import AssignmentResponseTypeEnum
|
|
12
|
+
from .common import UNSET, Unset
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .user import User
|
|
16
|
+
from .assignment_response_content import AssignmentResponseContent
|
|
17
|
+
from ...client import HiveClient
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T", bound="AssignmentResponse")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@define
|
|
24
|
+
class AssignmentResponse(HiveCoreItem):
|
|
25
|
+
"""
|
|
26
|
+
Attributes:
|
|
27
|
+
id (int):
|
|
28
|
+
user_id (int):
|
|
29
|
+
contents (list['AssignmentResponseContent']):
|
|
30
|
+
date (datetime.datetime):
|
|
31
|
+
response_type (AssignmentResponseTypeEnum):
|
|
32
|
+
* `Comment` - Comment
|
|
33
|
+
* `Work In Progress` - Workinprogress
|
|
34
|
+
* `Submission` - Submission
|
|
35
|
+
* `AutoCheck` - Autocheck
|
|
36
|
+
* `Redo` - Redo
|
|
37
|
+
* `Done` - Done
|
|
38
|
+
autocheck_statuses (Union[None, list['Status']]):
|
|
39
|
+
file_name (Union[Unset, str]):
|
|
40
|
+
dear_student (Union[Unset, bool]): Default: True.
|
|
41
|
+
hide_checker_name (Union[Unset, bool]):
|
|
42
|
+
segel_only (Union[Unset, bool]):
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
hive_client: "HiveClient"
|
|
46
|
+
assignment_id: int
|
|
47
|
+
id: int
|
|
48
|
+
user_id: int
|
|
49
|
+
contents: list["AssignmentResponseContent"]
|
|
50
|
+
date: datetime.datetime
|
|
51
|
+
response_type: AssignmentResponseTypeEnum
|
|
52
|
+
autocheck_statuses: Union[None, list["AutoCheckStatus"]]
|
|
53
|
+
file_name: Union[Unset, str] = UNSET
|
|
54
|
+
dear_student: Union[Unset, bool] = True
|
|
55
|
+
hide_checker_name: Union[Unset, bool] = UNSET
|
|
56
|
+
segel_only: Union[Unset, bool] = UNSET
|
|
57
|
+
|
|
58
|
+
# Lazy-loaded objects
|
|
59
|
+
_user: "User | None" = field(init=False, default=None)
|
|
60
|
+
_assignment: "Assignment | None" = field(init=False, default=None)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
contents = []
|
|
64
|
+
for contents_item_data in self.contents:
|
|
65
|
+
contents_item = contents_item_data.to_dict()
|
|
66
|
+
contents.append(contents_item)
|
|
67
|
+
|
|
68
|
+
autocheck_statuses: Union[None, list[dict[str, Any]]]
|
|
69
|
+
if isinstance(self.autocheck_statuses, list):
|
|
70
|
+
autocheck_statuses = []
|
|
71
|
+
for autocheck_statuses_type_0_item_data in self.autocheck_statuses:
|
|
72
|
+
autocheck_statuses_type_0_item = (
|
|
73
|
+
autocheck_statuses_type_0_item_data.to_dict()
|
|
74
|
+
)
|
|
75
|
+
autocheck_statuses.append(autocheck_statuses_type_0_item)
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
autocheck_statuses = self.autocheck_statuses
|
|
79
|
+
|
|
80
|
+
file_name = self.file_name
|
|
81
|
+
|
|
82
|
+
dear_student = self.dear_student
|
|
83
|
+
|
|
84
|
+
hide_checker_name = self.hide_checker_name
|
|
85
|
+
|
|
86
|
+
segel_only = self.segel_only
|
|
87
|
+
|
|
88
|
+
field_dict: dict[str, Any] = {}
|
|
89
|
+
field_dict.update(
|
|
90
|
+
{
|
|
91
|
+
"id": self.id,
|
|
92
|
+
"user": self.user,
|
|
93
|
+
"contents": contents,
|
|
94
|
+
"date": self.date.isoformat(),
|
|
95
|
+
"response_type": self.response_type.value,
|
|
96
|
+
"autocheck_statuses": autocheck_statuses,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
if file_name is not UNSET:
|
|
100
|
+
field_dict["file_name"] = file_name
|
|
101
|
+
if dear_student is not UNSET:
|
|
102
|
+
field_dict["dear_student"] = dear_student
|
|
103
|
+
if hide_checker_name is not UNSET:
|
|
104
|
+
field_dict["hide_checker_name"] = hide_checker_name
|
|
105
|
+
if segel_only is not UNSET:
|
|
106
|
+
field_dict["segel_only"] = segel_only
|
|
107
|
+
|
|
108
|
+
return field_dict
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_dict( # pylint: disable=too-many-locals, arguments-differ
|
|
112
|
+
cls: type[T],
|
|
113
|
+
src_dict: dict[str, Any],
|
|
114
|
+
assignment_id: int,
|
|
115
|
+
hive_client: "HiveClient",
|
|
116
|
+
) -> T:
|
|
117
|
+
from .assignment_response_content import (
|
|
118
|
+
AssignmentResponseContent,
|
|
119
|
+
) # pylint: disable=import-outside-toplevel
|
|
120
|
+
|
|
121
|
+
d = dict(src_dict)
|
|
122
|
+
id = d.pop("id")
|
|
123
|
+
|
|
124
|
+
user_id = d.pop("user")
|
|
125
|
+
|
|
126
|
+
contents = []
|
|
127
|
+
_contents = d.pop("contents")
|
|
128
|
+
if not isinstance(_contents, list):
|
|
129
|
+
raise TypeError(
|
|
130
|
+
f"Assignment response contents must be a list, not {type(_contents)}"
|
|
131
|
+
)
|
|
132
|
+
for contents_item_data in _contents:
|
|
133
|
+
contents_item = AssignmentResponseContent.from_dict(
|
|
134
|
+
contents_item_data,
|
|
135
|
+
assignment=assignment_id,
|
|
136
|
+
assignment_response_id=id,
|
|
137
|
+
hive_client=hive_client,
|
|
138
|
+
)
|
|
139
|
+
contents.append(contents_item)
|
|
140
|
+
date = isoparse(d.pop("date"))
|
|
141
|
+
response_type = AssignmentResponseTypeEnum(d.pop("response_type"))
|
|
142
|
+
|
|
143
|
+
def _parse_autocheck_statuses(
|
|
144
|
+
data: object,
|
|
145
|
+
) -> Union[None, list["AutoCheckStatus"]]:
|
|
146
|
+
if data is None:
|
|
147
|
+
return data
|
|
148
|
+
if not isinstance(data, list):
|
|
149
|
+
raise TypeError(f"Autocheck statuses must be a list, not {type(data)}")
|
|
150
|
+
autocheck_statuses_type_0 = []
|
|
151
|
+
_autocheck_statuses_type_0 = data
|
|
152
|
+
for autocheck_statuses_type_0_item_data in _autocheck_statuses_type_0:
|
|
153
|
+
autocheck_statuses_type_0_item = AutoCheckStatus.from_dict(
|
|
154
|
+
autocheck_statuses_type_0_item_data,
|
|
155
|
+
hive_client=hive_client,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
autocheck_statuses_type_0.append(autocheck_statuses_type_0_item)
|
|
159
|
+
|
|
160
|
+
return autocheck_statuses_type_0
|
|
161
|
+
|
|
162
|
+
autocheck_statuses = _parse_autocheck_statuses(d.pop("autocheck_statuses"))
|
|
163
|
+
|
|
164
|
+
file_name = d.pop("file_name", UNSET)
|
|
165
|
+
|
|
166
|
+
dear_student = d.pop("dear_student", UNSET)
|
|
167
|
+
|
|
168
|
+
hide_checker_name = d.pop("hide_checker_name", UNSET)
|
|
169
|
+
|
|
170
|
+
segel_only = d.pop("segel_only", UNSET)
|
|
171
|
+
|
|
172
|
+
return cls(
|
|
173
|
+
hive_client=hive_client,
|
|
174
|
+
assignment_id=assignment_id,
|
|
175
|
+
id=id,
|
|
176
|
+
user_id=user_id,
|
|
177
|
+
contents=contents,
|
|
178
|
+
date=date,
|
|
179
|
+
response_type=response_type,
|
|
180
|
+
autocheck_statuses=autocheck_statuses,
|
|
181
|
+
file_name=file_name,
|
|
182
|
+
dear_student=dear_student,
|
|
183
|
+
hide_checker_name=hide_checker_name,
|
|
184
|
+
segel_only=segel_only,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def user(self) -> "User":
|
|
189
|
+
"""Lazily load and return the user this assignment belongs to."""
|
|
190
|
+
if self._user is None:
|
|
191
|
+
self._user = self.hive_client.get_user(self.user_id)
|
|
192
|
+
return self._user
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def assignment(self) -> "Assignment":
|
|
196
|
+
"""Lazily load and return the assignment this response belongs to."""
|
|
197
|
+
if self._assignment is None:
|
|
198
|
+
self._assignment = self.hive_client.get_assignment(
|
|
199
|
+
assignment_id=self.assignment_id
|
|
200
|
+
)
|
|
201
|
+
return self._assignment
|
|
202
|
+
|
|
203
|
+
def __iter__(self) -> Generator["AssignmentResponseContent", None, None]:
|
|
204
|
+
"""Allow iteration over this AssignmentResponse to yield its contents."""
|
|
205
|
+
yield from self.contents
|