canvaslms 5.10__py3-none-any.whl → 6.1__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.
@@ -289,6 +289,12 @@ def assignments_view_command(config, canvas, args):
289
289
  assignment_rubric = None
290
290
  if hasattr(assignment, "rubric") and assignment.rubric:
291
291
  assignment_rubric = assignment.rubric
292
+ if hasattr(args, "assignment") and args.assignment not in (None, ".*"):
293
+ # User specified a -a filter, use it verbatim
294
+ assignment_regex = args.assignment
295
+ else:
296
+ # Generate exact-match regex from name
297
+ assignment_regex = "^" + re.escape(assignment.name) + "$"
292
298
  output = canvaslms.cli.content.render_to_html(
293
299
  assignment,
294
300
  canvaslms.cli.content.ASSIGNMENT_SCHEMA,
@@ -296,6 +302,7 @@ def assignments_view_command(config, canvas, args):
296
302
  extra_attributes={
297
303
  "modules": assignment_modules,
298
304
  "rubric": assignment_rubric,
305
+ "regex": assignment_regex,
299
306
  },
300
307
  )
301
308
  print(output)
@@ -306,6 +313,12 @@ def assignments_view_command(config, canvas, args):
306
313
  assignment_rubric = None
307
314
  if hasattr(assignment, "rubric") and assignment.rubric:
308
315
  assignment_rubric = assignment.rubric
316
+ if hasattr(args, "assignment") and args.assignment not in (None, ".*"):
317
+ # User specified a -a filter, use it verbatim
318
+ assignment_regex = args.assignment
319
+ else:
320
+ # Generate exact-match regex from name
321
+ assignment_regex = "^" + re.escape(assignment.name) + "$"
309
322
  output = canvaslms.cli.content.render_to_markdown(
310
323
  assignment,
311
324
  canvaslms.cli.content.ASSIGNMENT_SCHEMA,
@@ -313,6 +326,7 @@ def assignments_view_command(config, canvas, args):
313
326
  extra_attributes={
314
327
  "modules": assignment_modules,
315
328
  "rubric": assignment_rubric,
329
+ "regex": assignment_regex,
316
330
  },
317
331
  )
318
332
  print(output)
@@ -502,37 +516,42 @@ def assignments_edit_command(config, canvas, args):
502
516
  args.file
503
517
  )
504
518
  except FileNotFoundError:
505
- logger.error(f"File not found: {args.file}")
506
- print(f"Error: File not found: {args.file}", file=sys.stderr)
507
- return
519
+ canvaslms.cli.err(1, f"File not found: {args.file}")
508
520
  except Exception as e:
509
- logger.error(f"Error reading file: {e}")
510
- print(f"Error reading file: {e}", file=sys.stderr)
511
- return
521
+ canvaslms.cli.err(1, f"Error reading file: {e}")
512
522
 
513
523
  errors = canvaslms.cli.content.validate_attributes(
514
524
  attributes, canvaslms.cli.content.ASSIGNMENT_SCHEMA
515
525
  )
516
526
  if errors:
517
527
  for error in errors:
518
- print(f"Validation error: {error}", file=sys.stderr)
519
- return
528
+ logger.error(f"Validation error: {error}")
529
+ canvaslms.cli.err(1, "File validation failed")
520
530
  if args.html:
521
531
  html_content = body_content # Already HTML, no conversion needed
522
532
  else:
523
533
  html_content = pypandoc.convert_text(body_content, "html", format="md")
524
534
 
525
- if "id" in attributes and attributes["id"]:
526
- assignment_id = attributes["id"]
535
+ if "regex" in attributes and attributes["regex"]:
536
+ regex_pattern = attributes["regex"]
527
537
  course_list = courses.process_course_option(canvas, args)
528
538
  if not course_list:
529
- print("Error: No courses found matching criteria", file=sys.stderr)
530
- return
539
+ canvaslms.cli.err(1, "No courses found matching criteria")
531
540
 
532
541
  course = course_list[0]
542
+
543
+ all_assignments = list(course.get_assignments())
544
+ for a in all_assignments:
545
+ a.course = course
546
+
533
547
  try:
534
- assignment = course.get_assignment(assignment_id)
535
- assignment.course = course
548
+ assignment = canvaslms.cli.content.match_item_by_regex(
549
+ all_assignments,
550
+ regex_pattern,
551
+ [lambda a: a.name, lambda a: str(a.id)],
552
+ item_type="assignment",
553
+ name_func=lambda a: a.name,
554
+ )
536
555
  update_data = {
537
556
  "assignment": {
538
557
  "name": attributes.get("name", assignment.name),
@@ -584,12 +603,12 @@ def assignments_edit_command(config, canvas, args):
584
603
  file=sys.stderr,
585
604
  )
586
605
  except Exception as e:
587
- logger.error(f"Error updating assignment '{assignment.name}': {e}")
588
- print(
589
- f"Error updating assignment '{assignment.name}': {e}",
590
- file=sys.stderr,
606
+ canvaslms.cli.err(
607
+ 1, f"Error updating assignment '{assignment.name}': {e}"
591
608
  )
592
- except Exception as e:
609
+ except re.error as e:
610
+ canvaslms.cli.err(1, f"Invalid regex pattern '{regex_pattern}': {e}")
611
+ except canvaslms.cli.EmptyListError:
593
612
  if args.create:
594
613
  create_params = {
595
614
  "name": attributes.get("name", "Untitled Assignment"),
@@ -617,9 +636,9 @@ def assignments_edit_command(config, canvas, args):
617
636
  assignment=create_params
618
637
  )
619
638
  new_assignment.course = course
620
- print(
621
- f"Created assignment: {new_assignment.name} (id: {new_assignment.id})",
622
- file=sys.stderr,
639
+ logger.info(
640
+ f"Created assignment: {new_assignment.name} "
641
+ f"(id: {new_assignment.id})"
623
642
  )
624
643
  if "modules" in attributes:
625
644
  module_regexes = attributes["modules"]
@@ -635,75 +654,211 @@ def assignments_edit_command(config, canvas, args):
635
654
  file=sys.stderr,
636
655
  )
637
656
  except Exception as e:
638
- logger.error(f"Error creating assignment: {e}")
639
- print(f"Error creating assignment: {e}", file=sys.stderr)
657
+ canvaslms.cli.err(1, f"Error creating assignment: {e}")
640
658
  else:
641
- print(
642
- f"Error: Assignment with ID '{assignment_id}' not found. "
659
+ canvaslms.cli.err(
660
+ 1,
661
+ f"No assignments match regex '{regex_pattern}'. "
643
662
  f"Use --create to create a new assignment.",
644
- file=sys.stderr,
645
663
  )
646
- return
647
664
  else:
648
- assignment_list = process_assignment_option(canvas, args)
665
+ if "name" in attributes and attributes["name"]:
666
+ name = attributes["name"]
667
+ course_list = courses.process_course_option(canvas, args)
668
+ if not course_list:
669
+ canvaslms.cli.err(1, "No courses found matching criteria")
649
670
 
650
- for assignment in assignment_list:
651
- update_data = {
652
- "assignment": {
653
- "name": attributes.get("name", assignment.name),
654
- "description": html_content,
655
- }
656
- }
671
+ course = course_list[0]
657
672
 
658
- if "due_at" in attributes and attributes["due_at"] is not None:
659
- update_data["assignment"]["due_at"] = attributes["due_at"]
660
- if "unlock_at" in attributes and attributes["unlock_at"] is not None:
661
- update_data["assignment"]["unlock_at"] = attributes["unlock_at"]
662
- if "lock_at" in attributes and attributes["lock_at"] is not None:
663
- update_data["assignment"]["lock_at"] = attributes["lock_at"]
664
- if (
665
- "points_possible" in attributes
666
- and attributes["points_possible"] is not None
667
- ):
668
- update_data["assignment"]["points_possible"] = attributes[
669
- "points_possible"
670
- ]
671
- if "published" in attributes:
672
- update_data["assignment"]["published"] = attributes["published"]
673
+ all_assignments = list(course.get_assignments())
674
+ for a in all_assignments:
675
+ a.course = course
673
676
 
674
677
  try:
675
- assignment.edit(**update_data)
676
- assignment._fetched_at = datetime.now()
677
- if hasattr(assignment.course, "assignment_cache"):
678
- assignment.course.assignment_cache[assignment.id] = (
679
- assignment,
680
- {},
681
- )
682
- logger.info(f"Updated assignment: {assignment.name}")
683
- if "modules" in attributes:
684
- module_regexes = attributes["modules"]
685
- added, removed = modules.update_item_modules(
686
- assignment.course,
687
- "Assignment",
688
- assignment.id,
689
- module_regexes,
678
+ assignment = canvaslms.cli.content.match_item_by_name(
679
+ all_assignments, name, lambda a: a.name, item_type="assignment"
680
+ )
681
+ update_data = {
682
+ "assignment": {
683
+ "name": attributes.get("name", assignment.name),
684
+ "description": html_content,
685
+ }
686
+ }
687
+
688
+ if "due_at" in attributes and attributes["due_at"] is not None:
689
+ update_data["assignment"]["due_at"] = attributes["due_at"]
690
+ if (
691
+ "unlock_at" in attributes
692
+ and attributes["unlock_at"] is not None
693
+ ):
694
+ update_data["assignment"]["unlock_at"] = attributes["unlock_at"]
695
+ if "lock_at" in attributes and attributes["lock_at"] is not None:
696
+ update_data["assignment"]["lock_at"] = attributes["lock_at"]
697
+ if (
698
+ "points_possible" in attributes
699
+ and attributes["points_possible"] is not None
700
+ ):
701
+ update_data["assignment"]["points_possible"] = attributes[
702
+ "points_possible"
703
+ ]
704
+ if "published" in attributes:
705
+ update_data["assignment"]["published"] = attributes["published"]
706
+
707
+ try:
708
+ assignment.edit(**update_data)
709
+ assignment._fetched_at = datetime.now()
710
+ if hasattr(assignment.course, "assignment_cache"):
711
+ assignment.course.assignment_cache[assignment.id] = (
712
+ assignment,
713
+ {},
714
+ )
715
+ logger.info(f"Updated assignment: {assignment.name}")
716
+ if "modules" in attributes:
717
+ module_regexes = attributes["modules"]
718
+ added, removed = modules.update_item_modules(
719
+ assignment.course,
720
+ "Assignment",
721
+ assignment.id,
722
+ module_regexes,
723
+ )
724
+ if added:
725
+ print(
726
+ f" Added to modules: {', '.join(added)}",
727
+ file=sys.stderr,
728
+ )
729
+ if removed:
730
+ print(
731
+ f" Removed from modules: {', '.join(removed)}",
732
+ file=sys.stderr,
733
+ )
734
+ except Exception as e:
735
+ canvaslms.cli.err(
736
+ 1, f"Error updating assignment '{assignment.name}': {e}"
690
737
  )
691
- if added:
692
- print(
693
- f" Added to modules: {', '.join(added)}",
694
- file=sys.stderr,
738
+ except canvaslms.cli.EmptyListError:
739
+ if args.create:
740
+ create_params = {
741
+ "name": attributes.get("name", "Untitled Assignment"),
742
+ "description": html_content,
743
+ }
744
+ if "due_at" in attributes and attributes["due_at"] is not None:
745
+ create_params["due_at"] = attributes["due_at"]
746
+ if (
747
+ "unlock_at" in attributes
748
+ and attributes["unlock_at"] is not None
749
+ ):
750
+ create_params["unlock_at"] = attributes["unlock_at"]
751
+ if (
752
+ "lock_at" in attributes
753
+ and attributes["lock_at"] is not None
754
+ ):
755
+ create_params["lock_at"] = attributes["lock_at"]
756
+ if (
757
+ "points_possible" in attributes
758
+ and attributes["points_possible"] is not None
759
+ ):
760
+ create_params["points_possible"] = attributes[
761
+ "points_possible"
762
+ ]
763
+ if "published" in attributes:
764
+ create_params["published"] = attributes["published"]
765
+
766
+ try:
767
+ new_assignment = course.create_assignment(
768
+ assignment=create_params
695
769
  )
696
- if removed:
697
- print(
698
- f" Removed from modules: {', '.join(removed)}",
699
- file=sys.stderr,
770
+ new_assignment.course = course
771
+ logger.info(
772
+ f"Created assignment: {new_assignment.name} "
773
+ f"(id: {new_assignment.id})"
700
774
  )
701
- except Exception as e:
702
- logger.error(f"Error updating assignment '{assignment.name}': {e}")
703
- print(
704
- f"Error updating assignment '{assignment.name}': {e}",
705
- file=sys.stderr,
775
+ if "modules" in attributes:
776
+ module_regexes = attributes["modules"]
777
+ added, removed = modules.update_item_modules(
778
+ new_assignment.course,
779
+ "Assignment",
780
+ new_assignment.id,
781
+ module_regexes,
782
+ )
783
+ if added:
784
+ print(
785
+ f" Added to modules: {', '.join(added)}",
786
+ file=sys.stderr,
787
+ )
788
+ except Exception as e:
789
+ canvaslms.cli.err(1, f"Error creating assignment: {e}")
790
+ else:
791
+ canvaslms.cli.err(
792
+ 1,
793
+ f"No assignments match name '{name}'. "
794
+ f"Use --create to create a new assignment.",
795
+ )
796
+ else:
797
+ assignment_list = process_assignment_option(canvas, args)
798
+ if len(assignment_list) > 1:
799
+ canvaslms.cli.err(
800
+ 1,
801
+ f"{len(assignment_list)} assignments match filter. "
802
+ f"Add 'name' or 'regex' to YAML, or use -a to narrow selection.",
706
803
  )
804
+ for assignment in assignment_list:
805
+ update_data = {
806
+ "assignment": {
807
+ "name": attributes.get("name", assignment.name),
808
+ "description": html_content,
809
+ }
810
+ }
811
+
812
+ if "due_at" in attributes and attributes["due_at"] is not None:
813
+ update_data["assignment"]["due_at"] = attributes["due_at"]
814
+ if (
815
+ "unlock_at" in attributes
816
+ and attributes["unlock_at"] is not None
817
+ ):
818
+ update_data["assignment"]["unlock_at"] = attributes["unlock_at"]
819
+ if "lock_at" in attributes and attributes["lock_at"] is not None:
820
+ update_data["assignment"]["lock_at"] = attributes["lock_at"]
821
+ if (
822
+ "points_possible" in attributes
823
+ and attributes["points_possible"] is not None
824
+ ):
825
+ update_data["assignment"]["points_possible"] = attributes[
826
+ "points_possible"
827
+ ]
828
+ if "published" in attributes:
829
+ update_data["assignment"]["published"] = attributes["published"]
830
+
831
+ try:
832
+ assignment.edit(**update_data)
833
+ assignment._fetched_at = datetime.now()
834
+ if hasattr(assignment.course, "assignment_cache"):
835
+ assignment.course.assignment_cache[assignment.id] = (
836
+ assignment,
837
+ {},
838
+ )
839
+ logger.info(f"Updated assignment: {assignment.name}")
840
+ if "modules" in attributes:
841
+ module_regexes = attributes["modules"]
842
+ added, removed = modules.update_item_modules(
843
+ assignment.course,
844
+ "Assignment",
845
+ assignment.id,
846
+ module_regexes,
847
+ )
848
+ if added:
849
+ print(
850
+ f" Added to modules: {', '.join(added)}",
851
+ file=sys.stderr,
852
+ )
853
+ if removed:
854
+ print(
855
+ f" Removed from modules: {', '.join(removed)}",
856
+ file=sys.stderr,
857
+ )
858
+ except Exception as e:
859
+ canvaslms.cli.err(
860
+ 1, f"Error updating assignment '{assignment.name}': {e}"
861
+ )
707
862
  else:
708
863
  try:
709
864
  assignment_list = process_assignment_option(canvas, args)
@@ -891,6 +1046,8 @@ def add_assignments_command(subp):
891
1046
  "view",
892
1047
  help="View assignment details",
893
1048
  description="Views detailed information about assignments. "
1049
+ "When piped (not a TTY), outputs YAML front matter with a 'regex' field "
1050
+ "for stable identification, followed by the description as Markdown. "
894
1051
  "Use --html to preserve HTML instead of converting to Markdown.",
895
1052
  )
896
1053
  view_parser.set_defaults(func=assignments_view_command)
@@ -938,10 +1095,18 @@ def add_assignments_command(subp):
938
1095
  description="Edit assignment content. Without -f, opens each matching "
939
1096
  "assignment in your editor for interactive editing with preview. "
940
1097
  "With -f, reads from a Markdown file with YAML front matter and updates "
941
- "directly (script-friendly). If the YAML contains an 'id' field, the "
942
- "command uses it to identify the assignment; use --create to create a new "
943
- "assignment if the ID is not found. Use --html to read/edit HTML directly "
944
- "without Markdown conversion.",
1098
+ "directly (script-friendly). "
1099
+ "Assignment matching workflow: "
1100
+ "(1) If the YAML contains a 'regex' field, it matches assignments by "
1101
+ "name or Canvas numeric ID. The regex is tested against both. "
1102
+ "This provides stable identification: export an assignment with "
1103
+ "'assignments view -a \"Lab 1.*\" > lab.md', edit locally, and push "
1104
+ "back with 'assignments edit -f lab.md'---the regex stays constant "
1105
+ "even if the name changes slightly. "
1106
+ "(2) If no 'regex' is present but 'name' is, exact name matching is "
1107
+ "used. "
1108
+ "(3) Otherwise, falls back to -a, -A, and -M filters. "
1109
+ "Use --html to read/edit HTML directly without Markdown conversion.",
945
1110
  )
946
1111
  edit_parser.set_defaults(func=assignments_edit_command)
947
1112
  add_assignment_option(edit_parser, ungraded=False, required=True)
@@ -949,7 +1114,7 @@ def add_assignments_command(subp):
949
1114
  edit_parser.add_argument(
950
1115
  "--create",
951
1116
  action="store_true",
952
- help="Create a new assignment if the ID in the YAML is not found",
1117
+ help="Create a new assignment if no match is found",
953
1118
  )
954
1119
  edit_parser.add_argument(
955
1120
  "--html",
canvaslms/cli/cli.nw CHANGED
@@ -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
 
@@ -411,6 +421,11 @@ if args.func:
411
421
  err(1, str(e))
412
422
  else:
413
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)
414
429
  @
415
430
 
416
431
  We know that the [[login]] command doesn't need the Canvas object.