diff --git a/opaque_keys/__init__.py b/opaque_keys/__init__.py index f5f6c12..c288773 100644 --- a/opaque_keys/__init__.py +++ b/opaque_keys/__init__.py @@ -7,10 +7,12 @@ formats, and allowing new serialization formats to be installed transparently. """ from __future__ import annotations + +import typing as t from abc import ABCMeta, abstractmethod from collections import defaultdict +from dataclasses import dataclass from functools import total_ordering -from typing import Self from stevedore.enabled import EnabledExtensionManager @@ -26,20 +28,9 @@ def __init__(self, key_class, serialized): super().__init__(f'{key_class}: {serialized}') -class OpaqueKeyMetaclass(ABCMeta): - """ - Metaclass for :class:`OpaqueKey`. Sets the default value for the values in ``KEY_FIELDS`` to - ``None``. - """ - def __new__(mcs, name, bases, attrs): - if '__slots__' not in attrs: - for field in attrs.get('KEY_FIELDS', []): - attrs.setdefault(field, None) - return super().__new__(mcs, name, bases, attrs) - - @total_ordering -class OpaqueKey(metaclass=OpaqueKeyMetaclass): +@dataclass(slots=True) +class OpaqueKey: """ A base-class for implementing pluggable opaque keys. Individual key subclasses identify particular types of resources, without specifying the actual form of the key (or @@ -93,13 +84,12 @@ class constructor will not validate any of the ``KEY_FIELDS`` arguments, and wil Serialization of an :class:`OpaqueKey` is performed by using the :func:`unicode` builtin. Deserialization is performed by the :meth:`from_string` method. """ - __slots__ = ('_initialized', 'deprecated') - - KEY_FIELDS: tuple[str, ...] # pylint: disable=declare-non-slot - CANONICAL_NAMESPACE: str # pylint: disable=declare-non-slot - KEY_TYPE: str # pylint: disable=declare-non-slot - NAMESPACE_SEPARATOR = ':' - CHECKED_INIT: bool = True + _initialized: bool + deprecated: bool + CHECKED_INIT: t.ClassVar[bool] = True + NAMESPACE_SEPARATOR: t.ClassVar[str] = ':' + CANONICAL_NAMESPACE: t.ClassVar[str] + KEY_TYPE: t.ClassVar[str] # ============= ABSTRACT METHODS ============== @classmethod @@ -170,7 +160,7 @@ def __str__(self) -> str: return self.NAMESPACE_SEPARATOR.join([self.CANONICAL_NAMESPACE, self._to_string()]) @classmethod - def from_string(cls, serialized: str) -> Self: + def from_string(cls, serialized: str) -> t.Self: """ Return a :class:`OpaqueKey` object deserialized from the `serialized` argument. This object will be an instance @@ -242,7 +232,7 @@ def get_namespace_plugin(cls, namespace: str): # a particular unknown namespace (like i4x) raise InvalidKeyError(cls, f'{namespace}:*') from error - LOADED_DRIVERS: dict[type[OpaqueKey], EnabledExtensionManager] = defaultdict() + LOADED_DRIVERS: t.ClassVar[dict[type[OpaqueKey], EnabledExtensionManager]] = defaultdict() @classmethod def _drivers(cls: type[OpaqueKey]): diff --git a/opaque_keys/edx/locator.py b/opaque_keys/edx/locator.py index 3a20690..b0fa39b 100644 --- a/opaque_keys/edx/locator.py +++ b/opaque_keys/edx/locator.py @@ -1687,30 +1687,30 @@ class LibraryContainerLocator(CheckFieldMixin, ContainerKey): lct:org:lib:ct-type:ct-id """ CANONICAL_NAMESPACE = 'lct' # "Library Container" - KEY_FIELDS = ('lib_key', 'container_type', 'container_id') - lib_key: LibraryLocatorV2 + KEY_FIELDS = ('lib_key', 'container_type', 'container_code') + library_key: LibraryLocatorV2 container_type: str - container_id: str + container_code: str __slots__ = KEY_FIELDS CHECKED_INIT = False # Allow container IDs to contian unicode characters - CONTAINER_ID_REGEXP = re.compile(r'^[\w\-.]+$', flags=re.UNICODE) + CONTAINER_CODE_REGEXP = re.compile(r'^[\w\-.]+$', flags=re.UNICODE) - def __init__(self, lib_key: LibraryLocatorV2, container_type: str, container_id: str): + def __init__(self, library_key: LibraryLocatorV2, container_type: str, container_code: str): """ Construct a CollectionLocator """ - if not isinstance(lib_key, LibraryLocatorV2): + if not isinstance(library_key, LibraryLocatorV2): raise TypeError("lib_key must be a LibraryLocatorV2") self._check_key_string_field("container_type", container_type) - self._check_key_string_field("container_id", container_id, regexp=self.CONTAINER_ID_REGEXP) + self._check_key_string_field("container_code", container_code, regexp=self.CONTAINER_CODE_REGEXP) super().__init__( - lib_key=lib_key, + lib_key=library_key, container_type=container_type, - container_id=container_id, + container_code=container_code, ) @property @@ -1718,21 +1718,21 @@ def org(self) -> str | None: # pragma: no cover """ The organization that this Container belongs to. """ - return self.lib_key.org + return self.library_key.org @property def context_key(self) -> LibraryLocatorV2: - return self.lib_key + return self.library_key def _to_string(self) -> str: """ Serialize this key as a string """ return ":".join(( - self.lib_key.org, - self.lib_key.slug, + self.library_key.org, + self.library_key.slug, self.container_type, - self.container_id + self.container_code, )) @classmethod diff --git a/opaque_keys/edx/tests/test_container_locators.py b/opaque_keys/edx/tests/test_container_locators.py index 68fff54..e5fe0ce 100644 --- a/opaque_keys/edx/tests/test_container_locators.py +++ b/opaque_keys/edx/tests/test_container_locators.py @@ -33,11 +33,11 @@ def test_key_constructor(self): container_id = 'test-container' lib_key = LibraryLocatorV2(org=org, slug=lib) container_key = LibraryContainerLocator( - lib_key=lib_key, + library_key=lib_key, container_type=container_type, container_id=container_id, ) - lib_key = container_key.lib_key + lib_key = container_key.library_key assert str(container_key) == "lct:TestX:LibraryX:unit:test-container" assert container_key.org == org assert container_key.container_type == container_type @@ -50,16 +50,16 @@ def test_key_constructor_bad_ids(self): lib_key = LibraryLocatorV2(org="TestX", slug="lib1") with self.assertRaises(TypeError): - LibraryContainerLocator(lib_key=None, container_type='unit', container_id='usage') + LibraryContainerLocator(library_key=None, container_type='unit', container_id='usage') with self.assertRaises(ValueError): - LibraryContainerLocator(lib_key=lib_key, container_type='unit', container_id='usage-!@#{$%^&*}') + LibraryContainerLocator(library_key=lib_key, container_type='unit', container_id='usage-!@#{$%^&*}') def test_key_constructor_bad_type(self): lib_key = LibraryLocatorV2(org="TestX", slug="lib1") with self.assertRaises(ValueError): - LibraryContainerLocator(lib_key=lib_key, container_type='unit-!@#{$%^&*}', container_id='usage') + LibraryContainerLocator(library_key=lib_key, container_type='unit-!@#{$%^&*}', container_id='usage') def test_key_from_string(self): org = 'TestX' @@ -73,7 +73,7 @@ def test_key_from_string(self): assert container_key.org == org assert container_key.container_type == container_type assert container_key.container_id == container_id - lib_key = container_key.lib_key + lib_key = container_key.library_key assert isinstance(lib_key, LibraryLocatorV2) assert lib_key.org == org assert lib_key.slug == lib diff --git a/openedx_keys/__init__.py b/openedx_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openedx_keys/api.py b/openedx_keys/api.py new file mode 100644 index 0000000..5a43e42 --- /dev/null +++ b/openedx_keys/api.py @@ -0,0 +1,74 @@ +""" +Public, supported API for keys in the Open edX platform. +""" + +# pylint: disable=wildcard-import,unused-import + +from opaque_keys import ( + InvalidKeyError, + OpaqueKey, + # EXCLUDED: + # OpaqueKeyMetaclass (implementation detail) +) +from opaque_keys.edx.asides import ( + AsideDefinitionKeyV1, + AsideDefinitionKeyV2, + AsideUsageKeyV1, + AsideUsageKeyV2, +) +from opaque_keys.edx.block_types import ( + BlockTypeKeyV1, + # EXCLUDED: + # XBLOCK_V1 (unused) + # XMODULE_V1 (unused and deprecated) +) +from opaque_keys.edx.django.models import ( + BlockTypeKeyField, + CollectionKeyField, + ContainerKeyField, + CourseKeyField, + LearningContextKeyField, + UsageKeyField, + # EXCLUDED + # CreatorMixin (unused) + # OpaqueKeyField (unused and silly) + # OpaqueKeyFieldEmptyLookupIsNull (unused) + # LocationKeyField (deprecated) +) +from opaque_keys.edx.keys import ( + AsideDefinitionKey, + AsideUsageKey, + AssetKey, + BlockTypeKey, + ContainerKey, + LearningContextKey as ContextKey, + CourseKey as CourselikeKey, + UsageKey, + UsageKeyV2 as ContentUsageKey, + # EXCLUDED: + # CourseObjectMixin (should be private) + # CollectionKey (implementation detail of CollectionKeyV2) + # DefinitionKey (implementation detail of DefinitionKeyV1) + # i4xEncoder (deprecated) +) +from opaque_keys.edx.locator import ( + LibraryCollectionLocator as CollectionKey, + AssetLocator as CourseRunAssetKey, + DefinitionLocator as CourseRunDefinitionKey, + CourseLocator as CourseRunKey, + BlockUsageLocator as CourseRunUsageKey, + LibraryLocator as LegacyLibraryKey, + LibraryUsageLocator as LecacyLibraryUsageKey, + LibraryLocatorV2 as LibraryKey, + LibraryContainerLocator as LibraryContainerKey, + LibraryUsageLocatorV2 as LibraryUsageKey, + LocalId, + # EXCLUDED: + # BlockLocatorBase (implementation detail) + # BundleDefinitionLocator (deprecated) + # CheckFieldMixin (should be private) + # Locator (implementation detail) + # VersionTree (unused) +) +# EXCLUDED: +# opaque_keys.edx.location (deprecated) diff --git a/openedx_keys/newkeys.py b/openedx_keys/newkeys.py new file mode 100644 index 0000000..63d75b4 --- /dev/null +++ b/openedx_keys/newkeys.py @@ -0,0 +1,107 @@ +# keys.py +import typing as t +from dataclasses import dataclass + + +# Abstract abstract base keys + +@dataclass(frozen=True) +class ParsableKey: + def from_string(self, key_string: str) -> t.Self: + raise NotImplementedError + +@dataclass(frozen=True) +class PluggableKey(ParsableKey): + @property + def prefix(self) -> str: + raise NotImplementedError + + +@dataclass(frozen=True) +class ContextKey(PluggableKey): + pass + +@dataclass(frozen=True) +class UsageKey(PluggableKey): + def context_key(self) -> ContextKey: + raise NotImplementedError + + +# Abstract base keys for Packages and the things in them. +# Not tied to Libraries. + +@dataclass(frozen=True) +class PackageKey(ParsableKey): + pass + +@dataclass(frozen=True) +class CollectionKey(ParsableKey): + @property + def package_key(self) -> PackageKey: + raise NotImplementedError + @property + def collection_code(self) -> str: + raise NotImplementedError + +@dataclass(frozen=True) +class EntityKey(ParsableKey): + @property + def package_key(self) -> PackageKey: + raise NotImplementedError + @property + def type_code(self) -> str: + raise NotImplementedError + @property + def entity_code(self) -> str: + raise NotImplementedError + +@dataclass(frozen=True) +class ComponentKey(EntityKey): + type_namespace: str + type_name: str + @property + def type_code(self): + return f"{self.type_name}:{self.type_namsepace}" + +@dataclass(frozen=True) +class ContainerKey(EntityKey): + pass + + +# Concrete keys for Libraries and the things in them. +@dataclass(frozen=True) +class LibraryKey(PackageKey, ContextKey): + """lib:{org}:{library_code}""" + prefix: t.ClassVar[str] = "lib" + org: str + library_code: str + +# (Abstract) +@dataclass(frozen=True) +class LibraryEntityKey(EntityKey): + library_key: LibraryKey + @property + def package_key(self) -> PackageKey: + return self.library_key + +class LibraryCollectionKey(CollectionKey): + """lib-collection:{library_key.org}:{library_key.library_code}:{collection_code}""" + prefix: t.ClassVar[str] = "lib-collection" + library_key: LibraryKey + collection_code: str + +class LibraryComponentKey(LibraryEntityKey, ComponentKey): + """lb:{library_key.org}:{library_key.library_code}:{type_namespace}:{type_name}:{entity_code}""" + prefix: t.ClassVar[str] = "lb" + pass + +class LibraryContainerKey(LibraryEntityKey, ContainerKey): + """lct:{library_key.org}:{library_key.library_code}:{type_code}:{container_code}""" + prefix: t.ClassVar[str] = "lct" + pass + +# Context&Usage versions of Library keys so that we can preview items in the authoring environment +class LibraryPreviewContextKey(LibraryKey, ContextKey): + pass +class LibraryPreviewUsageKey(LibraryEntityKey, ContextKey) + pass \ No newline at end of file diff --git a/setup.py b/setup.py index b17d1b9..2969b5c 100644 --- a/setup.py +++ b/setup.py @@ -157,34 +157,34 @@ def get_version(*file_paths): 'dict = opaque_keys.tests.test_opaque_keys:DictKey', ], 'context_key': [ - 'course-v1 = opaque_keys.edx.locator:CourseLocator', - 'library-v1 = opaque_keys.edx.locator:LibraryLocator', - 'lib = opaque_keys.edx.locator:LibraryLocatorV2', + 'course-v1 = openedx_keys.api:CourseKeyV1', + 'library-v1 = openedx_keys.api:LibraryKeyV1', + 'lib = openedx_keys.api:LibraryKeyV2', # don't use slashes in any new code - 'slashes = opaque_keys.edx.locator:CourseLocator', + 'slashes = openedx_keys.api:CourseKeyV1', ], 'usage_key': [ - 'block-v1 = opaque_keys.edx.locator:BlockUsageLocator', - 'lib-block-v1 = opaque_keys.edx.locator:LibraryUsageLocator', - 'lb = opaque_keys.edx.locator:LibraryUsageLocatorV2', + 'block-v1 = openedx_keys.api:UsageKeyV1', + 'lib-block-v1 = openedx_keys.api:LibraryKeyV1', + 'lb = openedx_keys.api:LibraryUsageKeyV2',, 'location = opaque_keys.edx.locations:DeprecatedLocation', - 'aside-usage-v1 = opaque_keys.edx.asides:AsideUsageKeyV1', - 'aside-usage-v2 = opaque_keys.edx.asides:AsideUsageKeyV2', + 'aside-usage-v1 = openedx_keys.api:AsideUsageKeyV1', + 'aside-usage-v2 = opaque_keys.api:AsideUsageKeyV2', ], 'asset_key': [ - 'asset-v1 = opaque_keys.edx.locator:AssetLocator', + 'asset-v1 = openedx_keys.api:AssetKeyV1', ], 'definition_key': [ - 'def-v1 = opaque_keys.edx.locator:DefinitionLocator', - 'aside-def-v1 = opaque_keys.edx.asides:AsideDefinitionKeyV1', - 'aside-def-v2 = opaque_keys.edx.asides:AsideDefinitionKeyV2', + 'def-v1 = openedx_keys.api:DefinitionKeyV1', + 'aside-def-v1 = openedx_keys.api:AsideDefinitionKeyV1', + 'aside-def-v2 = openedx_keys.api:AsideDefinitionKeyV2', 'bundle-olx = opaque_keys.edx.locator:BundleDefinitionLocator', ], 'block_type': [ 'block-type-v1 = opaque_keys.edx.block_types:BlockTypeKeyV1', ], 'collection_key': [ - 'lib-collection = opaque_keys.edx.locator:LibraryCollectionLocator', + 'lib-collection = openedx_keys.api:CollectionKeyV2', ], 'container_key': [ 'lct = opaque_keys.edx.locator:LibraryContainerLocator',