canvaslms 5.9__tar.gz → 6.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.
Files changed (113) hide show
  1. {canvaslms-5.9 → canvaslms-6.0}/PKG-INFO +1 -1
  2. {canvaslms-5.9 → canvaslms-6.0}/pyproject.toml +1 -1
  3. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/assignments.nw +151 -49
  4. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/cli.nw +41 -3
  5. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/content.nw +290 -14
  6. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/pages.nw +130 -77
  7. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/quizzes.nw +58 -4
  8. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/results.nw +85 -45
  9. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/users.nw +64 -1
  10. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/conjunctavg.nw +50 -1
  11. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/conjunctavgsurvey.nw +50 -0
  12. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/disjunctmax.nw +59 -0
  13. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/grades.nw +17 -3
  14. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/maxgradesurvey.nw +58 -0
  15. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/participation.nw +27 -0
  16. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/tilkryLAB1.nw +47 -0
  17. {canvaslms-5.9 → canvaslms-6.0}/LICENSE +0 -0
  18. {canvaslms-5.9 → canvaslms-6.0}/README.md +0 -0
  19. {canvaslms-5.9 → canvaslms-6.0}/makefiles/.circleci/config.yml +0 -0
  20. {canvaslms-5.9 → canvaslms-6.0}/makefiles/.git +0 -0
  21. {canvaslms-5.9 → canvaslms-6.0}/makefiles/.gitignore +0 -0
  22. {canvaslms-5.9 → canvaslms-6.0}/makefiles/CONTRIBUTING.md +0 -0
  23. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Dockerfile +0 -0
  24. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Dockerfile.nw +0 -0
  25. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Dockerfile.tex +0 -0
  26. {canvaslms-5.9 → canvaslms-6.0}/makefiles/LICENSE +0 -0
  27. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Makefile +0 -0
  28. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Makefile.nw +0 -0
  29. {canvaslms-5.9 → canvaslms-6.0}/makefiles/Makefile.tex +0 -0
  30. {canvaslms-5.9 → canvaslms-6.0}/makefiles/README.md +0 -0
  31. {canvaslms-5.9 → canvaslms-6.0}/makefiles/doc.mk +0 -0
  32. {canvaslms-5.9 → canvaslms-6.0}/makefiles/doc.mk.nw +0 -0
  33. {canvaslms-5.9 → canvaslms-6.0}/makefiles/doc.tex +0 -0
  34. {canvaslms-5.9 → canvaslms-6.0}/makefiles/exam.bib +0 -0
  35. {canvaslms-5.9 → canvaslms-6.0}/makefiles/exam.mk +0 -0
  36. {canvaslms-5.9 → canvaslms-6.0}/makefiles/exam.mk.nw +0 -0
  37. {canvaslms-5.9 → canvaslms-6.0}/makefiles/exam.tex +0 -0
  38. {canvaslms-5.9 → canvaslms-6.0}/makefiles/gitattributes +0 -0
  39. {canvaslms-5.9 → canvaslms-6.0}/makefiles/haskell.mk +0 -0
  40. {canvaslms-5.9 → canvaslms-6.0}/makefiles/haskell.mk.nw +0 -0
  41. {canvaslms-5.9 → canvaslms-6.0}/makefiles/haskell.tex +0 -0
  42. {canvaslms-5.9 → canvaslms-6.0}/makefiles/intro.tex +0 -0
  43. {canvaslms-5.9 → canvaslms-6.0}/makefiles/latexmkrc +0 -0
  44. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/_minted/AAA0D43723DCC5BE9DB71A96B52C1142.highlight.minted +0 -0
  45. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/_minted/E5724293DA12A769F97F0E91498CAEEF.highlight.minted +0 -0
  46. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/_minted/FFC039B25D180E99FC9FEBDB2D42EBAF.highlight.minted +0 -0
  47. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/_minted/_7945F0071C510291A619CE1658F673CB.index.minted +0 -0
  48. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/_minted/default.style.minted +0 -0
  49. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.aux +0 -0
  50. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.bbl +0 -0
  51. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.bcf +0 -0
  52. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.blg +0 -0
  53. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.fdb_latexmk +0 -0
  54. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.fls +0 -0
  55. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.log +0 -0
  56. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.out +0 -0
  57. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.pdf +0 -0
  58. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.run.xml +0 -0
  59. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.toc +0 -0
  60. {canvaslms-5.9 → canvaslms-6.0}/makefiles/ltxobj/makefiles.xdv +0 -0
  61. {canvaslms-5.9 → canvaslms-6.0}/makefiles/makefiles.bib +0 -0
  62. {canvaslms-5.9 → canvaslms-6.0}/makefiles/makefiles.tex +0 -0
  63. {canvaslms-5.9 → canvaslms-6.0}/makefiles/miun.course.mk +0 -0
  64. {canvaslms-5.9 → canvaslms-6.0}/makefiles/miun.depend.mk +0 -0
  65. {canvaslms-5.9 → canvaslms-6.0}/makefiles/miun.docs.mk +0 -0
  66. {canvaslms-5.9 → canvaslms-6.0}/makefiles/miun.port.mk +0 -0
  67. {canvaslms-5.9 → canvaslms-6.0}/makefiles/miun.pub.mk +0 -0
  68. {canvaslms-5.9 → canvaslms-6.0}/makefiles/noweb.mk +0 -0
  69. {canvaslms-5.9 → canvaslms-6.0}/makefiles/noweb.mk.nw +0 -0
  70. {canvaslms-5.9 → canvaslms-6.0}/makefiles/noweb.tex +0 -0
  71. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pkg.mk +0 -0
  72. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pkg.mk.nw +0 -0
  73. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pkg.tex +0 -0
  74. {canvaslms-5.9 → canvaslms-6.0}/makefiles/portability.mk +0 -0
  75. {canvaslms-5.9 → canvaslms-6.0}/makefiles/portability.mk.nw +0 -0
  76. {canvaslms-5.9 → canvaslms-6.0}/makefiles/portability.tex +0 -0
  77. {canvaslms-5.9 → canvaslms-6.0}/makefiles/preamble.tex +0 -0
  78. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pub.mk +0 -0
  79. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pub.mk.nw +0 -0
  80. {canvaslms-5.9 → canvaslms-6.0}/makefiles/pub.tex +0 -0
  81. {canvaslms-5.9 → canvaslms-6.0}/makefiles/results.mk +0 -0
  82. {canvaslms-5.9 → canvaslms-6.0}/makefiles/results.mk.nw +0 -0
  83. {canvaslms-5.9 → canvaslms-6.0}/makefiles/results.tex +0 -0
  84. {canvaslms-5.9 → canvaslms-6.0}/makefiles/subdir.mk +0 -0
  85. {canvaslms-5.9 → canvaslms-6.0}/makefiles/subdir.mk.nw +0 -0
  86. {canvaslms-5.9 → canvaslms-6.0}/makefiles/subdir.tex +0 -0
  87. {canvaslms-5.9 → canvaslms-6.0}/makefiles/tex.bib +0 -0
  88. {canvaslms-5.9 → canvaslms-6.0}/makefiles/tex.mk +0 -0
  89. {canvaslms-5.9 → canvaslms-6.0}/makefiles/tex.mk.nw +0 -0
  90. {canvaslms-5.9 → canvaslms-6.0}/makefiles/tex.tex +0 -0
  91. {canvaslms-5.9 → canvaslms-6.0}/makefiles/transform.bib +0 -0
  92. {canvaslms-5.9 → canvaslms-6.0}/makefiles/transform.mk +0 -0
  93. {canvaslms-5.9 → canvaslms-6.0}/makefiles/transform.mk.nw +0 -0
  94. {canvaslms-5.9 → canvaslms-6.0}/makefiles/transform.tex +0 -0
  95. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/Makefile +0 -0
  96. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/__init__.py +0 -0
  97. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/Makefile +0 -0
  98. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/cache.nw +0 -0
  99. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/calendar.nw +0 -0
  100. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/courses.nw +0 -0
  101. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/discussions.nw +0 -0
  102. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/grade.nw +0 -0
  103. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/login.nw +0 -0
  104. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/modules.nw +0 -0
  105. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/results.py.broken +0 -0
  106. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/submissions.nw +0 -0
  107. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/cli/utils.nw +0 -0
  108. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/Makefile +0 -0
  109. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/grades/grades.py +0 -0
  110. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/hacks/Makefile +0 -0
  111. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/hacks/__init__.py +0 -0
  112. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/hacks/attachment_cache.nw +0 -0
  113. {canvaslms-5.9 → canvaslms-6.0}/src/canvaslms/hacks/canvasapi.nw +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canvaslms
3
- Version: 5.9
3
+ Version: 6.0
4
4
  Summary: Command-line interface to Canvas LMS
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "canvaslms"
3
- version = "5.9"
3
+ version = "6.0"
4
4
  description = "Command-line interface to Canvas LMS"
5
5
  authors = [
6
6
  {name = "Daniel Bosk", email = "daniel@bosk.se"}
@@ -381,6 +381,8 @@ and pushed back without losing any formatting.
381
381
  view_parser = assignments_subp.add_parser("view",
382
382
  help="View assignment details",
383
383
  description="Views detailed information about assignments. "
384
+ "When piped (not a TTY), outputs YAML front matter with a 'regex' field "
385
+ "for stable identification, followed by the description as Markdown. "
384
386
  "Use --html to preserve HTML instead of converting to Markdown.")
385
387
  view_parser.set_defaults(func=assignments_view_command)
386
388
  add_assignment_option(view_parser)
@@ -418,16 +420,24 @@ edit_parser = assignments_subp.add_parser("edit",
418
420
  description="Edit assignment content. Without -f, opens each matching "
419
421
  "assignment in your editor for interactive editing with preview. "
420
422
  "With -f, reads from a Markdown file with YAML front matter and updates "
421
- "directly (script-friendly). If the YAML contains an 'id' field, the "
422
- "command uses it to identify the assignment; use --create to create a new "
423
- "assignment if the ID is not found. Use --html to read/edit HTML directly "
424
- "without Markdown conversion.")
423
+ "directly (script-friendly). "
424
+ "Assignment matching workflow: "
425
+ "(1) If the YAML contains a 'regex' field, it matches assignments by "
426
+ "name or Canvas numeric ID. The regex is tested against both. "
427
+ "This provides stable identification: export an assignment with "
428
+ "'assignments view -a \"Lab 1.*\" > lab.md', edit locally, and push "
429
+ "back with 'assignments edit -f lab.md'---the regex stays constant "
430
+ "even if the name changes slightly. "
431
+ "(2) If no 'regex' is present but 'name' is, exact name matching is "
432
+ "used. "
433
+ "(3) Otherwise, falls back to -a, -A, and -M filters. "
434
+ "Use --html to read/edit HTML directly without Markdown conversion.")
425
435
  edit_parser.set_defaults(func=assignments_edit_command)
426
436
  add_assignment_option(edit_parser, ungraded=False, required=True)
427
437
  canvaslms.cli.content.add_file_option(edit_parser)
428
438
  edit_parser.add_argument("--create",
429
439
  action="store_true",
430
- help="Create a new assignment if the ID in the YAML is not found")
440
+ help="Create a new assignment if no match is found")
431
441
  edit_parser.add_argument("--html",
432
442
  action="store_true",
433
443
  help="Read file as HTML instead of converting from Markdown. "
@@ -633,11 +643,14 @@ contain this assignment. When editing:
633
643
  <<output assignment in editable format>>=
634
644
  <<get current modules for assignment>>
635
645
  <<get rubric for assignment>>
646
+ <<determine regex for assignment>>
636
647
  output = canvaslms.cli.content.render_to_markdown(
637
648
  assignment,
638
649
  canvaslms.cli.content.ASSIGNMENT_SCHEMA,
639
650
  content_attr='description',
640
- extra_attributes={'modules': assignment_modules, 'rubric': assignment_rubric})
651
+ extra_attributes={'modules': assignment_modules,
652
+ 'rubric': assignment_rubric,
653
+ 'regex': assignment_regex})
641
654
  print(output)
642
655
  @
643
656
 
@@ -657,6 +670,19 @@ assignment_modules = modules.get_item_modules(
657
670
  assignment.course, 'Assignment', assignment.id)
658
671
  @
659
672
 
673
+ When determining the regex for export, we follow the same approach as for pages.
674
+ If the user specified a non-default [[-a]] pattern, we use that regex verbatim
675
+ (capturing their intent). Otherwise, we generate an exact-match regex from the
676
+ assignment name by escaping special characters and adding anchors.
677
+ <<determine regex for assignment>>=
678
+ if hasattr(args, 'assignment') and args.assignment not in (None, '.*'):
679
+ # User specified a -a filter, use it verbatim
680
+ assignment_regex = args.assignment
681
+ else:
682
+ # Generate exact-match regex from name
683
+ assignment_regex = '^' + re.escape(assignment.name) + '$'
684
+ @
685
+
660
686
  \subsection{HTML output format}
661
687
 
662
688
  When [[--html]] is specified, we preserve the original HTML content instead of
@@ -691,11 +717,14 @@ and pushed back using [[assignments edit --html -f]].
691
717
  <<output assignment in html format>>=
692
718
  <<get current modules for assignment>>
693
719
  <<get rubric for assignment>>
720
+ <<determine regex for assignment>>
694
721
  output = canvaslms.cli.content.render_to_html(
695
722
  assignment,
696
723
  canvaslms.cli.content.ASSIGNMENT_SCHEMA,
697
724
  content_attr='description',
698
- extra_attributes={'modules': assignment_modules, 'rubric': assignment_rubric})
725
+ extra_attributes={'modules': assignment_modules,
726
+ 'rubric': assignment_rubric,
727
+ 'regex': assignment_regex})
699
728
  print(output)
700
729
  @
701
730
 
@@ -1032,69 +1061,91 @@ try:
1032
1061
  # body_content is Markdown or HTML depending on args.html
1033
1062
  attributes, body_content = canvaslms.cli.content.read_content_from_file(args.file)
1034
1063
  except FileNotFoundError:
1035
- logger.error(f"File not found: {args.file}")
1036
- print(f"Error: File not found: {args.file}", file=sys.stderr)
1037
- return
1064
+ canvaslms.cli.err(1, f"File not found: {args.file}")
1038
1065
  except Exception as e:
1039
- logger.error(f"Error reading file: {e}")
1040
- print(f"Error reading file: {e}", file=sys.stderr)
1041
- return
1066
+ canvaslms.cli.err(1, f"Error reading file: {e}")
1042
1067
 
1043
1068
  errors = canvaslms.cli.content.validate_attributes(
1044
1069
  attributes, canvaslms.cli.content.ASSIGNMENT_SCHEMA)
1045
1070
  if errors:
1046
1071
  for error in errors:
1047
- print(f"Validation error: {error}", file=sys.stderr)
1048
- return
1072
+ logger.error(f"Validation error: {error}")
1073
+ canvaslms.cli.err(1, "File validation failed")
1049
1074
  @
1050
1075
 
1051
1076
  \subsection{Updating assignment with new content}
1052
1077
 
1053
1078
  We convert the content to HTML for Canvas (unless [[--html]] is specified, in
1054
- which case the content is already HTML). If the YAML front matter contains an
1055
- [[id]] field, we use it to identify the specific assignment to update. This
1056
- enables a Git-based workflow where the assignment can be reliably identified
1057
- even if the name changes. If the ID is not found and [[--create]] is specified,
1058
- we create a new assignment instead.
1059
-
1060
- If no [[id]] is in the YAML, we fall back to the filter-based matching using
1061
- [[-a]], [[-A]], and [[-M]] options (the existing behavior).
1079
+ which case the content is already HTML).
1080
+
1081
+ The matching strategy prioritizes reliability and safety:
1082
+ \begin{enumerate}
1083
+ \item If the YAML contains a [[regex]] field, we compile it and match against
1084
+ assignment names and Canvas numeric IDs. This provides stable identification
1085
+ even when names change, as long as the new name still matches the pattern.
1086
+ \item If no [[regex]] is in the YAML but a [[name]] is present, we use exact
1087
+ name matching. This is safe because it won't accidentally match multiple
1088
+ assignments.
1089
+ \item As a last resort (for backward compatibility), we fall back to
1090
+ filter-based matching using [[-a]], [[-A]], and [[-M]] options only if no
1091
+ name is specified.
1092
+ \end{enumerate}
1062
1093
  <<update assignment with new content>>=
1063
1094
  if args.html:
1064
1095
  html_content = body_content # Already HTML, no conversion needed
1065
1096
  else:
1066
1097
  html_content = pypandoc.convert_text(body_content, 'html', format='md')
1067
1098
 
1068
- if 'id' in attributes and attributes['id']:
1069
- <<update or create assignment by id>>
1099
+ if 'regex' in attributes and attributes['regex']:
1100
+ <<update or create assignment by regex>>
1070
1101
  else:
1071
1102
  <<update assignments by filter matching>>
1072
1103
  @
1073
1104
 
1074
- \subsubsection{ID-based assignment identification}
1105
+ \subsubsection{Regex-based assignment identification}
1075
1106
 
1076
- When the YAML contains an [[id]] field, we look up the assignment directly by
1077
- its Canvas ID. This is more reliable than filter matching because it identifies
1078
- exactly one assignment even if multiple assignments have similar names.
1079
- <<update or create assignment by id>>=
1080
- assignment_id = attributes['id']
1107
+ When the YAML contains a [[regex]] field, we use [[match_item_by_regex]] from
1108
+ the content module to find the matching assignment. The function handles the
1109
+ three-way dispatch (one match, zero matches, multiple matches) and raises
1110
+ [[EmptyListError]] or [[MultipleMatchesError]] as appropriate.
1111
+
1112
+ We match the regex against both assignment names and Canvas numeric IDs,
1113
+ providing flexibility: an assignment can be identified by a pattern in its name
1114
+ (\enquote{Lab 1.*}) or by its numeric ID (\verb|^12345$|).
1115
+
1116
+ If exactly one assignment matches, we update it. If no assignments match and
1117
+ [[--create]] is specified, we create a new one. If multiple assignments match,
1118
+ the exception message lists the matching names so the user can make the regex
1119
+ more specific.
1120
+ <<update or create assignment by regex>>=
1121
+ regex_pattern = attributes['regex']
1081
1122
  course_list = courses.process_course_option(canvas, args)
1082
1123
  if not course_list:
1083
- print("Error: No courses found matching criteria", file=sys.stderr)
1084
- return
1124
+ canvaslms.cli.err(1, "No courses found matching criteria")
1085
1125
 
1086
1126
  course = course_list[0]
1127
+
1128
+ all_assignments = list(course.get_assignments())
1129
+ for a in all_assignments:
1130
+ a.course = course
1131
+
1087
1132
  try:
1088
- assignment = course.get_assignment(assignment_id)
1089
- assignment.course = course
1133
+ assignment = canvaslms.cli.content.match_item_by_regex(
1134
+ all_assignments, regex_pattern,
1135
+ [lambda a: a.name, lambda a: str(a.id)],
1136
+ item_type="assignment",
1137
+ name_func=lambda a: a.name)
1090
1138
  <<update existing assignment>>
1091
- except Exception as e:
1139
+ except re.error as e:
1140
+ canvaslms.cli.err(
1141
+ 1, f"Invalid regex pattern '{regex_pattern}': {e}")
1142
+ except canvaslms.cli.EmptyListError:
1092
1143
  if args.create:
1093
1144
  <<create new assignment>>
1094
1145
  else:
1095
- print(f"Error: Assignment with ID '{assignment_id}' not found. "
1096
- f"Use --create to create a new assignment.", file=sys.stderr)
1097
- return
1146
+ canvaslms.cli.err(1,
1147
+ f"No assignments match regex '{regex_pattern}'. "
1148
+ f"Use --create to create a new assignment.")
1098
1149
  @
1099
1150
 
1100
1151
  When updating an existing assignment, we build the update dictionary and call
@@ -1115,8 +1166,8 @@ try:
1115
1166
  logger.info(f"Updated assignment: {assignment.name}")
1116
1167
  <<update assignment module membership>>
1117
1168
  except Exception as e:
1118
- logger.error(f"Error updating assignment '{assignment.name}': {e}")
1119
- print(f"Error updating assignment '{assignment.name}': {e}", file=sys.stderr)
1169
+ canvaslms.cli.err(1,
1170
+ f"Error updating assignment '{assignment.name}': {e}")
1120
1171
  @
1121
1172
 
1122
1173
  \subsubsection{Creating new assignments}
@@ -1142,11 +1193,11 @@ if 'published' in attributes:
1142
1193
  try:
1143
1194
  new_assignment = course.create_assignment(assignment=create_params)
1144
1195
  new_assignment.course = course
1145
- print(f"Created assignment: {new_assignment.name} (id: {new_assignment.id})", file=sys.stderr)
1196
+ logger.info(f"Created assignment: {new_assignment.name} "
1197
+ f"(id: {new_assignment.id})")
1146
1198
  <<update new assignment module membership>>
1147
1199
  except Exception as e:
1148
- logger.error(f"Error creating assignment: {e}")
1149
- print(f"Error creating assignment: {e}", file=sys.stderr)
1200
+ canvaslms.cli.err(1, f"Error creating assignment: {e}")
1150
1201
  @
1151
1202
 
1152
1203
  For newly created assignments, we also process module membership if specified.
@@ -1161,14 +1212,65 @@ if 'modules' in attributes:
1161
1212
 
1162
1213
  \subsubsection{Filter-based assignment matching}
1163
1214
 
1164
- When no ID is specified in the YAML, we use the filter options ([[-a]], [[-A]],
1165
- [[-M]]) to find matching assignments. This maintains backwards compatibility
1166
- with the original behavior.
1215
+ When no [[regex]] is specified in the YAML, we check whether a [[name]] is
1216
+ present. If so, we use exact name matching to find the assignment---this is
1217
+ critical for safety. Without it, the command would fall back to filter-based
1218
+ matching with the default [[-a '.*']] pattern, which matches \emph{all}
1219
+ assignments in the course. The result would be catastrophic: every assignment
1220
+ would be overwritten with the content from the file.
1221
+
1222
+ If no [[name]] is present either, we fall back to filter-based matching using
1223
+ the [[-a]], [[-A]], and [[-M]] options (the original behavior). To prevent
1224
+ accidental mass-updates, we refuse to proceed if the filter matches more than
1225
+ one assignment.
1167
1226
  <<update assignments by filter matching>>=
1168
- assignment_list = process_assignment_option(canvas, args)
1227
+ if 'name' in attributes and attributes['name']:
1228
+ <<update or create assignment by name>>
1229
+ else:
1230
+ assignment_list = process_assignment_option(canvas, args)
1231
+ if len(assignment_list) > 1:
1232
+ canvaslms.cli.err(1,
1233
+ f"{len(assignment_list)} assignments match filter. "
1234
+ f"Add 'name' or 'regex' to YAML, or use -a to narrow selection.")
1235
+ for assignment in assignment_list:
1236
+ <<update existing assignment>>
1237
+ @
1169
1238
 
1170
- for assignment in assignment_list:
1239
+ \subsubsection{Name-based assignment identification}
1240
+
1241
+ When no [[regex]] is specified in the YAML but a [[name]] is present, we use
1242
+ [[match_item_by_name]] from the content module for exact name matching. This
1243
+ avoids the catastrophic scenario where the default [[-a '.*']] pattern matches
1244
+ all assignments and overwrites them all with the file content.
1245
+
1246
+ If exactly one assignment matches, we update it. If none match and [[--create]]
1247
+ is specified, we create a new assignment. If multiple assignments share the same
1248
+ name, the exception tells the user to add a [[regex]] field to disambiguate.
1249
+ <<update or create assignment by name>>=
1250
+ name = attributes['name']
1251
+ course_list = courses.process_course_option(canvas, args)
1252
+ if not course_list:
1253
+ canvaslms.cli.err(1, "No courses found matching criteria")
1254
+
1255
+ course = course_list[0]
1256
+
1257
+ all_assignments = list(course.get_assignments())
1258
+ for a in all_assignments:
1259
+ a.course = course
1260
+
1261
+ try:
1262
+ assignment = canvaslms.cli.content.match_item_by_name(
1263
+ all_assignments, name,
1264
+ lambda a: a.name,
1265
+ item_type="assignment")
1171
1266
  <<update existing assignment>>
1267
+ except canvaslms.cli.EmptyListError:
1268
+ if args.create:
1269
+ <<create new assignment>>
1270
+ else:
1271
+ canvaslms.cli.err(1,
1272
+ f"No assignments match name '{name}'. "
1273
+ f"Use --create to create a new assignment.")
1172
1274
  @
1173
1275
 
1174
1276
  We add optional attributes only if they are present in the YAML front matter.
@@ -76,9 +76,19 @@ import argparse
76
76
  \section{Printing errors and warnings}
77
77
 
78
78
  We want uniform error handling.
79
+ Both [[EmptyListError]] and [[MultipleMatchesError]] represent query-result
80
+ conditions: the caller asked for items and got the wrong number.
81
+ Different callers handle these differently---interactive commands may catch
82
+ them locally and continue, while batch commands let them propagate to the
83
+ main handler for uniform treatment.
79
84
  <<classes and exceptions>>=
80
85
  class EmptyListError(Exception):
81
- """Exception raised when a process function returns an empty list"""
86
+ """Exception raised when a query returns no matching items"""
87
+ pass
88
+
89
+ class MultipleMatchesError(Exception):
90
+ """Exception raised when a query matches multiple items
91
+ but exactly one was expected"""
82
92
  pass
83
93
  @
84
94
 
@@ -356,6 +366,19 @@ argp.add_argument("-v", "--verbose",
356
366
  @
357
367
 
358
368
 
369
+ \subsection{Cache control}
370
+
371
+ Most commands speed up by caching the Canvas API object between runs.
372
+ Occasionally you want to bypass the cache for a single command,
373
+ for example right after creating or updating objects.
374
+
375
+ <<add global options to argp>>=
376
+ argp.add_argument("--no-cache",
377
+ action="store_true",
378
+ help="Do not read or write the persistent Canvas object cache")
379
+ @
380
+
381
+
359
382
  \subsection{Logging in and setting up Canvas}
360
383
 
361
384
  Each subcommand will have its own module in the package.
@@ -398,6 +421,11 @@ if args.func:
398
421
  err(1, str(e))
399
422
  else:
400
423
  sys.exit(1)
424
+ except MultipleMatchesError as e:
425
+ if args.quiet == 0:
426
+ err(1, str(e))
427
+ else:
428
+ sys.exit(1)
401
429
  @
402
430
 
403
431
  We know that the [[login]] command doesn't need the Canvas object.
@@ -445,15 +473,25 @@ Before creating a new Canvas object from scratch, we try to load it from the
445
473
  persistent cache.
446
474
  This can significantly speed up commands by reusing previously fetched course
447
475
  data, assignments, users, and submissions.
476
+ If the cached object becomes stale (for example, right after creating a quiz),
477
+ you can bypass the cache with [[--no-cache]].
478
+
479
+ For example:
480
+ \begin{verbatim}
481
+ canvaslms --no-cache quizzes export -c "My Course" -a "My Quiz"
482
+ \end{verbatim}
448
483
  <<try to load canvas from cache>>=
449
- canvas = canvaslms.cli.cache.load_canvas_cache(token, hostname)
484
+ if args.no_cache:
485
+ canvas = None
486
+ else:
487
+ canvas = canvaslms.cli.cache.load_canvas_cache(token, hostname)
450
488
  @
451
489
 
452
490
  After successfully executing a command, we save the Canvas object to the cache
453
491
  for future use.
454
492
  We only save if the canvas object was actually used (not [[None]]).
455
493
  <<save canvas to cache after command>>=
456
- if canvas:
494
+ if canvas and not args.no_cache:
457
495
  canvaslms.cli.cache.save_canvas_cache(canvas, token, hostname)
458
496
  @
459
497