ed-api-client 0.1.7__tar.gz → 0.1.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ed-api-client
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A client for interacting with the Ed LMS
5
5
  License: MIT
6
6
  Author: David Milne
@@ -214,9 +214,6 @@ class ChallengeResult:
214
214
  The number of attempts that the student submitted for this challenge.
215
215
  completed : bool
216
216
  True if the student successfully completed this challenge, otherwise false.
217
- best_submission_by_calendar_days_late : List[Submission]
218
- The best submission received at each number of days late.
219
- Index 0 = on time, index 1 = up to 1 day late, etc.
220
217
  scores_by_calendar_days_late : List[int]
221
218
  The best score received at each number of days late.
222
219
  Index 0 = on time, index 1 = up to 1 day late, etc.
@@ -229,16 +226,16 @@ class ChallengeResult:
229
226
  max_days_late: int = 7
230
227
  attempts: int = 0
231
228
  completed: bool = False
232
- best_submission_by_calendar_days_late: List[Submission] = None
233
229
  scores_by_calendar_days_late: List[int] = None
234
230
  first_attempt: datetime.datetime = None
235
231
  last_attempt: datetime.datetime = None
232
+ _submissions_by_days_late: List[List[Submission]] = None
233
+ _best_submission_by_days_late: List[Submission] = None
236
234
 
237
235
  def add(self, submission: Submission, days_late: int):
238
236
  """
239
237
  Record a submission for this challenge.
240
238
 
241
- Keeps track of the best submission received at each number of days late.
242
239
  Call this for every submission before calling ``finalize``.
243
240
 
244
241
  Parameters
@@ -249,9 +246,10 @@ class ChallengeResult:
249
246
  How many calendar days after the deadline this submission was received.
250
247
  Pass 0 for on-time submissions.
251
248
  """
252
- if self.best_submission_by_calendar_days_late is None:
253
- self.best_submission_by_calendar_days_late = [None] * (self.max_days_late + 1)
249
+ if self._submissions_by_days_late is None:
254
250
  self.scores_by_calendar_days_late = [0] * (self.max_days_late + 1)
251
+ self._submissions_by_days_late = [[] for _ in range(self.max_days_late + 1)]
252
+ self._best_submission_by_days_late = [None] * (self.max_days_late + 1)
255
253
 
256
254
  self.attempts += 1
257
255
  if (
@@ -266,12 +264,7 @@ class ChallengeResult:
266
264
  self.last_attempt = submission.marked_at
267
265
 
268
266
  if days_late <= self.max_days_late:
269
- if (
270
- self.best_submission_by_calendar_days_late[days_late] is None
271
- or self.best_submission_by_calendar_days_late[days_late].test_cases_passed
272
- < submission.test_cases_passed
273
- ):
274
- self.best_submission_by_calendar_days_late[days_late] = submission
267
+ self._submissions_by_days_late[days_late].append(submission)
275
268
 
276
269
  def finalize(self, marker: ChallengeMarker, crawler):
277
270
  """
@@ -286,25 +279,34 @@ class ChallengeResult:
286
279
  ``BY_TESTCASE_SCORE``. May be None for other mark types.
287
280
  """
288
281
  for days_late in range(0, self.max_days_late + 1):
289
- submission = self.best_submission_by_calendar_days_late[days_late]
290
- if submission is None:
282
+ submissions = self._submissions_by_days_late[days_late]
283
+ if not submissions:
291
284
  continue
292
285
 
293
286
  if marker.mark_type is MarkType.BY_CHALLENGE:
294
- if (
295
- submission.status == "passed"
296
- and submission.test_cases_passed == submission.test_cases_total
297
- ):
287
+ best = max(
288
+ submissions,
289
+ key=lambda s: (
290
+ s.status == "passed" and s.test_cases_passed == s.test_cases_total,
291
+ s.test_cases_passed,
292
+ ),
293
+ )
294
+ self._best_submission_by_days_late[days_late] = best
295
+ if best.status == "passed" and best.test_cases_passed == best.test_cases_total:
298
296
  self.scores_by_calendar_days_late[days_late] = marker.points_per_challenge
299
297
  elif marker.mark_type is MarkType.BY_TESTCASE:
300
- self.scores_by_calendar_days_late[days_late] += submission.test_cases_passed
298
+ best = max(submissions, key=lambda s: s.test_cases_passed)
299
+ self._best_submission_by_days_late[days_late] = best
300
+ self.scores_by_calendar_days_late[days_late] = best.test_cases_passed
301
301
  elif marker.mark_type is MarkType.BY_TESTCASE_SCORE:
302
- s = crawler.get_submission(submission.id)
303
- for test in s.testcases:
304
- if test.passed:
305
- self.scores_by_calendar_days_late[days_late] = (
306
- self.scores_by_calendar_days_late[days_late] + test.score
307
- )
302
+ best_score = 0
303
+ for candidate in submissions:
304
+ s = crawler.get_submission(candidate.id)
305
+ score = sum(tc.score for tc in s.testcases if tc.passed)
306
+ if score > best_score:
307
+ best_score = score
308
+ self._best_submission_by_days_late[days_late] = s
309
+ self.scores_by_calendar_days_late[days_late] = best_score
308
310
  else:
309
311
  raise Exception(f"Invalid marker type {marker.mark_type}")
310
312
 
@@ -348,7 +350,7 @@ class ChallengeResult:
348
350
 
349
351
  for day in range(0, len(self.scores_by_calendar_days_late)):
350
352
  score = self.scores_by_calendar_days_late[day]
351
- submission = self.best_submission_by_calendar_days_late[day]
353
+ submission = self._best_submission_by_days_late[day]
352
354
  if submission is None:
353
355
  continue
354
356
 
@@ -473,6 +473,7 @@ class FileSummary(File):
473
473
  _insert_intervals: List[float] = field(default_factory=list)
474
474
  _prev_insert_time: datetime.datetime = None
475
475
  _pending_deletion: str = ""
476
+ _insert_frontier: int = 0
476
477
 
477
478
  def from_file(file: File):
478
479
  fs = FileSummary(file.id, file.path, file.content)
@@ -522,9 +523,8 @@ class WorkspaceSummary:
522
523
  log: WorkspaceLog,
523
524
  acceptable_selections: List[str] = [],
524
525
  stop_at_first_successful_submission: bool = True,
525
- max_idle_seconds=30,
526
+ max_idle_seconds=600,
526
527
  min_suspicious_paste_chars=50,
527
- max_continuous_typing_seconds=2,
528
528
  ):
529
529
  """
530
530
  Build a WorkspaceSummary by replaying a workspace event log.
@@ -569,6 +569,9 @@ class WorkspaceSummary:
569
569
  for selection in acceptable_selections:
570
570
  selected_content.add(selection)
571
571
 
572
+ session_active_time = datetime.timedelta(0)
573
+ prev_session_event_time = None
574
+
572
575
  curr_selection = None
573
576
  pending_selection = None
574
577
  for event in log.events:
@@ -595,6 +598,12 @@ class WorkspaceSummary:
595
598
  if event.type != "cursor" and event.type != "edit" and event.type != "init":
596
599
  continue
597
600
 
601
+ if prev_session_event_time is not None:
602
+ elapsed = event.at - prev_session_event_time
603
+ if elapsed.total_seconds() < max_idle_seconds:
604
+ session_active_time += elapsed
605
+ prev_session_event_time = event.at
606
+
598
607
  file = file_summaries.get(event.file_id)
599
608
  if file is None:
600
609
  continue
@@ -627,12 +636,8 @@ class WorkspaceSummary:
627
636
 
628
637
  pos = 0
629
638
 
630
- is_uninterrupted = False
631
639
  if file.prev_typing_time is not None:
632
640
  elapsed = event.at - file.prev_typing_time
633
- if elapsed.total_seconds() < max_continuous_typing_seconds:
634
- is_uninterrupted = True
635
-
636
641
  if elapsed.total_seconds() < max_idle_seconds:
637
642
  file.typing_time += elapsed
638
643
 
@@ -654,6 +659,12 @@ class WorkspaceSummary:
654
659
  )
655
660
  )
656
661
 
662
+ delete_end = pos + edit.value
663
+ if delete_end <= file._insert_frontier:
664
+ file._insert_frontier -= edit.value
665
+ elif pos < file._insert_frontier:
666
+ file._insert_frontier = pos # deletion overlaps the frontier
667
+
657
668
  file.content = file.content[:pos] + file.content[pos + edit.value :]
658
669
  file.transcribed_mask = (
659
670
  file.transcribed_mask[:pos]
@@ -673,10 +684,9 @@ class WorkspaceSummary:
673
684
  )
674
685
  )
675
686
 
676
- if (
677
- is_uninterrupted
678
- and get_code_character_count(file.content[pos:]) < 5
679
- ):
687
+ frontier_tolerance = max(10, len(file.content) // 10)
688
+ at_frontier = pos >= file._insert_frontier - frontier_tolerance
689
+ if at_frontier:
680
690
  file.transcribed_mask = (
681
691
  file.transcribed_mask[:pos]
682
692
  + get_masked_section(edit.value)
@@ -688,6 +698,11 @@ class WorkspaceSummary:
688
698
  + edit.value
689
699
  + file.transcribed_mask[pos:]
690
700
  )
701
+ n = len(edit.value)
702
+ if pos < file._insert_frontier:
703
+ file._insert_frontier += n # inserted before frontier — shift it right
704
+ else:
705
+ file._insert_frontier = pos + n # advance frontier to end of this insert
691
706
 
692
707
  if len(edit.value) > min_suspicious_paste_chars:
693
708
  if not selected_content.contains(edit.value):
@@ -719,10 +734,9 @@ class WorkspaceSummary:
719
734
  if '\n' in sel_text:
720
735
  sel_file.multiline_selection_count += 1
721
736
 
722
- total_active_time = datetime.timedelta(0)
737
+ total_active_time = session_active_time
723
738
  total_typing_time = datetime.timedelta(0)
724
739
  for file in file_summaries.values():
725
- total_active_time += file.active_time
726
740
  total_typing_time += file.typing_time
727
741
 
728
742
  pasted_sections = get_pasted_sections(
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ed-api-client"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "A client for interacting with the Ed LMS"
5
5
  authors = ["David Milne <d.n.milne@gmail.com>"]
6
6
  license = "MIT"
File without changes