@@ -425,11 +425,12 @@ class ContentLibraryData(ScopeData):
425425 Content libraries use the LibraryLocatorV2 format for identification.
426426
427427 Attributes:
428- NAMESPACE: 'lib' for content library scopes.
429- external_key: The content library identifier (e.g., 'lib:DemoX:CSPROB').
428+ NAMESPACE (str): 'lib' for content library scopes.
429+ ID_SEPARATOR (str): ':' for content library scopes.
430+ external_key (str): The content library identifier (e.g., 'lib:DemoX:CSPROB').
430431 Must be a valid LibraryLocatorV2 format.
431- namespaced_key: The library identifier with namespace (e.g., 'lib^lib:DemoX:CSPROB').
432- library_id: Property alias for external_key.
432+ namespaced_key (str) : The library identifier with namespace (e.g., 'lib^lib:DemoX:CSPROB').
433+ library_id (str) : Property alias for external_key.
433434
434435 Examples:
435436 >>> library = ContentLibraryData(external_key='lib:DemoX:CSPROB')
@@ -443,6 +444,7 @@ class ContentLibraryData(ScopeData):
443444 """
444445
445446 NAMESPACE : ClassVar [str ] = "lib"
447+ ID_SEPARATOR : ClassVar [str ] = ":"
446448
447449 @property
448450 def library_id (self ) -> str :
@@ -527,25 +529,27 @@ class OrgLibraryGlobData(ContentLibraryData):
527529 """Organization-level glob pattern for content libraries.
528530
529531 This class represents glob patterns that match multiple libraries within an organization.
530- Format: 'lib:ORG*' where ORG is a valid organization identifier.
532+ Format: 'lib:ORG: *' where ORG is a valid organization identifier.
531533
532534 The glob pattern allows granting permissions to all libraries within a specific organization
533535 without needing to specify each library individually.
534536
535537 Attributes:
536538 NAMESPACE (str): Inherited 'lib' from ContentLibraryData.
537- external_key (str): The glob pattern (e.g., 'lib:DemoX*').
538- namespaced_key (str): The pattern with namespace (e.g., 'lib^lib:DemoX*').
539+ ID_SEPARATOR (str): ':' for content library scopes.
540+ IS_GLOB (bool): True for organization-level glob patterns.
541+ external_key (str): The glob pattern (e.g., 'lib:DemoX:*').
542+ namespaced_key (str): The pattern with namespace (e.g., 'lib^lib:DemoX:*').
539543
540544 Validation Rules:
541545 - Must end with GLOBAL_SCOPE_WILDCARD (*)
542- - Must have format 'lib:ORG*' (exactly one organization identifier)
546+ - Must have format 'lib:ORG: *' (exactly one organization identifier)
543547 - The organization must exist in at least one ContentLibrary
544548 - Wildcard can only appear at the end after org identifier
545549 - Cannot have wildcards at slug level (lib:ORG:SLUG* is invalid)
546550
547551 Examples:
548- >>> glob = OrgLibraryGlobData(external_key='lib:DemoX*')
552+ >>> glob = OrgLibraryGlobData(external_key='lib:DemoX: *')
549553 >>> glob.org
550554 'DemoX'
551555
@@ -561,7 +565,7 @@ def org(self) -> str | None:
561565 """Get the organization identifier from the glob pattern.
562566
563567 Returns:
564- str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX*'), None otherwise.
568+ str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX: *'), None otherwise.
565569 """
566570 return self .get_org (self .external_key )
567571
@@ -570,15 +574,17 @@ def validate_external_key(cls, external_key: str) -> bool:
570574 """Validate the external_key format for organization-level library globs.
571575
572576 Args:
573- external_key (str): The external key to validate (e.g., 'lib:DemoX*').
577+ external_key (str): The external key to validate (e.g., 'lib:DemoX: *').
574578
575579 Returns:
576580 bool: True if the format is valid, False otherwise.
577581 """
578582 if not external_key .startswith (cls .NAMESPACE + EXTERNAL_KEY_SEPARATOR ):
579583 return False
580584
581- if not external_key .endswith (GLOBAL_SCOPE_WILDCARD ):
585+ # Enforce explicit org-level separator: 'lib:ORG:*'
586+ suffix = cls .ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
587+ if not external_key .endswith (suffix ):
582588 return False
583589
584590 org = cls .get_org (external_key )
@@ -598,9 +604,13 @@ def get_org(cls, external_key: str) -> str | None:
598604 external_key (str): The external key to extract the organization identifier from.
599605
600606 Returns:
601- str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX*'), None otherwise.
607+ str: The organization identifier (e.g., 'DemoX' from 'lib:DemoX: *'), None otherwise.
602608 """
603- scope_prefix = external_key [: - len (GLOBAL_SCOPE_WILDCARD )]
609+ suffix = cls .ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
610+ if not external_key .endswith (suffix ):
611+ return None
612+
613+ scope_prefix = external_key [: - len (suffix )]
604614 parts = scope_prefix .split (EXTERNAL_KEY_SEPARATOR )
605615
606616 if len (parts ) != 2 or not parts [1 ]:
@@ -650,22 +660,25 @@ class CourseOverviewData(ScopeData):
650660 Courses uses the CourseKey format for identification.
651661
652662 Attributes:
653- NAMESPACE: 'course-v1' for course scopes.
654- external_key: The course identifier (e.g., 'course-v1:TestOrg+TestCourse+2024_T1').
663+ NAMESPACE (str): 'course-v1' for course scopes.
664+ ID_SEPARATOR (str): '+' for course scopes.
665+ IS_GLOB (bool): False for course scopes.
666+ external_key (str): The course identifier (e.g., 'course-v1:TestOrg+TestCourse+2024_T1').
655667 Must be a valid CourseKey format.
656- namespaced_key: The course identifier with namespace (e.g., 'course-v1^course-v1:TestOrg+TestCourse+2024_T1').
657- course_id: Property alias for external_key.
668+ namespaced_key (str): The course identifier with namespace
669+ (e.g., 'course-v1^course-v1:TestOrg+TestCourse+2024_T1').
670+ course_id (str): Property alias for external_key.
658671
659672 Examples:
660673 >>> course = CourseOverviewData(external_key='course-v1:TestOrg+TestCourse+2024_T1')
661674 >>> course.namespaced_key
662675 'course-v1^course-v1:TestOrg+TestCourse+2024_T1'
663676 >>> course.course_id
664677 'course-v1:TestOrg+TestCourse+2024_T1'
665-
666678 """
667679
668680 NAMESPACE : ClassVar [str ] = "course-v1"
681+ ID_SEPARATOR : ClassVar [str ] = "+"
669682
670683 @property
671684 def course_id (self ) -> str :
@@ -750,25 +763,27 @@ class OrgCourseGlobData(CourseOverviewData):
750763 """Organization-level glob pattern for courses.
751764
752765 This class represents glob patterns that match multiple courses within an organization.
753- Format: 'course-v1:ORG*' where ORG is a valid organization identifier.
766+ Format: 'course-v1:ORG+ *' where ORG is a valid organization identifier.
754767
755768 The glob pattern allows granting permissions to all courses within a specific organization
756769 without needing to specify each course individually.
757770
758771 Attributes:
759- NAMESPACE: Inherited 'course-v1' from CourseOverviewData.
760- external_key: The glob pattern (e.g., 'course-v1:OpenedX*').
761- namespaced_key: The pattern with namespace (e.g., 'course-v1^course-v1:OpenedX*').
772+ NAMESPACE (str): Inherited 'course-v1' from CourseOverviewData.
773+ ID_SEPARATOR (str): '+' for course scopes.
774+ IS_GLOB (bool): True for organization-level glob patterns.
775+ external_key (str): The glob pattern (e.g., 'course-v1:OpenedX+*').
776+ namespaced_key (str): The pattern with namespace (e.g., 'course-v1^course-v1:OpenedX+*').
762777
763778 Validation Rules:
764779 - Must end with GLOBAL_SCOPE_WILDCARD (*)
765- - Must have format 'course-v1:ORG*' (exactly one organization identifier)
780+ - Must have format 'course-v1:ORG+ *' (exactly one organization identifier)
766781 - The organization must exist in at least one CourseOverview
767782 - Wildcard can only appear at the end after org identifier
768783 - Cannot have wildcards at course or run level (course-v1:ORG+COURSE* is invalid)
769784
770785 Examples:
771- >>> glob = OrgCourseGlobData(external_key='course-v1:OpenedX*')
786+ >>> glob = OrgCourseGlobData(external_key='course-v1:OpenedX+ *')
772787 >>> glob.org
773788 'OpenedX'
774789
@@ -784,7 +799,7 @@ def org(self) -> str | None:
784799 """Get the organization identifier from the glob pattern.
785800
786801 Returns:
787- str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX*'), None otherwise.
802+ str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX+ *'), None otherwise.
788803 """
789804 return self .get_org (self .external_key )
790805
@@ -793,15 +808,17 @@ def validate_external_key(cls, external_key: str) -> bool:
793808 """Validate the external_key format for organization-level course globs.
794809
795810 Args:
796- external_key (str): The external key to validate (e.g., 'course-v1:OpenedX*').
811+ external_key (str): The external key to validate (e.g., 'course-v1:OpenedX+ *').
797812
798813 Returns:
799814 bool: True if the format is valid, False otherwise.
800815 """
801816 if not external_key .startswith (cls .NAMESPACE + EXTERNAL_KEY_SEPARATOR ):
802817 return False
803818
804- if not external_key .endswith (GLOBAL_SCOPE_WILDCARD ):
819+ # Enforce explicit org-level separator: 'course-v1:ORG+*'
820+ glob_suffix = cls .ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
821+ if not external_key .endswith (glob_suffix ):
805822 return False
806823
807824 org = cls .get_org (external_key )
@@ -821,9 +838,13 @@ def get_org(cls, external_key: str) -> str | None:
821838 external_key (str): The external key to extract the organization identifier from.
822839
823840 Returns:
824- str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX*'), None otherwise.
841+ str | None: The organization identifier (e.g., 'OpenedX' from 'course-v1:OpenedX+ *'), None otherwise.
825842 """
826- scope_prefix = external_key [: - len (GLOBAL_SCOPE_WILDCARD )]
843+ suffix = cls .ID_SEPARATOR + GLOBAL_SCOPE_WILDCARD
844+ if not external_key .endswith (suffix ):
845+ return None
846+
847+ scope_prefix = external_key [: - len (suffix )]
827848 parts = scope_prefix .split (EXTERNAL_KEY_SEPARATOR )
828849
829850 if len (parts ) != 2 or not parts [1 ]:
0 commit comments