Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 13 additions & 23 deletions opaque_keys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]):
Expand Down
28 changes: 14 additions & 14 deletions opaque_keys/edx/locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1687,52 +1687,52 @@ 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
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
Expand Down
12 changes: 6 additions & 6 deletions opaque_keys/edx/tests/test_container_locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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
Expand Down
Empty file added openedx_keys/__init__.py
Empty file.
74 changes: 74 additions & 0 deletions openedx_keys/api.py
Original file line number Diff line number Diff line change
@@ -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)
107 changes: 107 additions & 0 deletions openedx_keys/newkeys.py
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading