Key Coherency for openedx-core v1.0

Key Coherency for openedx-core v1.0

There are more radical changes being floated in https://github.com/openedx/opaque-keys/issues/411 , but I want to focus on two things which I think we could get done in time for openedx-core v1.0.

This will be an… OEP?

1. Consistent names for key-like things

I’m turning this part into an OEP: https://github.com/openedx/openedx-proposals/pull/773

I think we should standardize on what we call the variety of identifiers that we have flying around the platform. As core developers, we know how to context-switch between “entity keys” and “opaque keys” and “primary keys” but I think that’s unreasonable to expect platform developers (or LLMs ;) to do correctly.

Here’s my idea. It would mostly be find-and-replace, focusing on making openedx-core correct rather than on fixing openedx-platform as a whole. The two hard things to fix would be openedx-core database columns and the library backup format.

term

meaning

example use

example value

term

meaning

example use

example value

pk, *_pk (preferred)

id, *_id (OK but ambiguous)

Database primary key.

 

CourseRun.catalog_course_id

def get_container(pk: int)

5

key, *_key

OpaqueKey object (parsed)

A parsed key

course_key

usage_key

course-v1:Axim+Chem101+Feb2026

lb:Axim:ChemLib:html:molecule6

key_string, *_key_string

OpaqueKey string (serialized)

course_key_string

usage_key_string

"course-v1:Axim+Chem101+Feb2026"

"lb:Axim:ChemLib:html:molecule6"

code, *_code

 

Alternative proposal:

slug, *_slug

A short string identifier, often used as a building block to an opaque key, but which is not an opaque key on its own.

org_code(formerly org)

course_code(formerly course or number)

run_code(formerly run)

library_code(formerly lib_slug)

type_code(formerly block_type)

component_code(formerly local_key)

block_code(formerly block_id)

collection_code(formerly collection_key)

"Axim"

"Chem101"

"Feb2026"

"ChemLib"

"html"

"molecule6"

"molecule6"

"molecules-basics"

label, *_label

 

@Braden MacDonald

@Dave Ormsbee (Axim)

Follow up

String which names something within openedx_content, either globally or locally. Should be suitable for use in URLs and file names. Nice if they are semi-human-readable.

These are often identical to or derived from OpaqueKeys, but are not themselves OpaqueKeys. Furthermore, their formats are only by-convention, so they should not be parsed!

LearningPackage.label

(formerly LearningPackage.key)

PublishableEntity.label

(formerly PublishableEntity.key)

"lib:blahOrg:blahLib"

 

"xblock.v1:html:molecule6"

 

Proposals:

”molecules”

”unit:molecules”

”unit-molecules”

”container.v1:unit:molecules”

2. Drop the term “Locator”

My understanding of the difference between a Key and a Locator within opaque-keys is that:

  • a Key class is abstract. It defines an entrypoint via its KEY_TYPE which can be plugged into by its subclasses. Optionally it can define abstract methods for children to define. Keys can also be abstract subclasses of other Keys. But, the concrete subclasses of other Keys are always called Locators.

  • a Locator class is concrete and constructible. It’s a subclass of a Key. It must be plugged into the parser via its superclass Key’s entrypoint using its CANONICAL_NAMESPACE (i.e., its prefix) and can also be plugged in via other prefixes for backcompat.

Example:

  • class LearningContextKey(OpaqueKey) , defines KEY_TYPE = context_key

    • class CourseKey(LearningContextKey)

      • class CourseLocator(CourseKey), defines CANONICAL_NAMESPACE = course-v1

      • class LibraryLocator(CourseKey), defines CANONICAL_NAMESPACE = library-v1

    • class LibraryLocatorV2(LearningContextKey), defines CANONICAL_NAMESPACE = lib

  • setup.py registers the locators into the context_key entrypoint:

    • entry_points={ ..., 'context_key': [ 'course-v1 = opaque_keys.edx.locator:CourseLocator', 'library-v1 = opaque_keys.edx.locator:LibraryLocator', 'lib = opaque_keys.edx.locator:LibraryLocatorV2', # don't use slashes in any new code 'slashes = opaque_keys.edx.locator:CourseLocator', ], ..., }

So, the confusing answer to the question “what’s the difference between locators and keys?” is:

  • every Key object is also a Locator object

  • every Locator object is also a Key object

  • Locator classes are the concrete subclasses of Key classes

I think very very few devs understand this distinction. I finally grokked it last week. I don’t think it’s a helpful distinction.

Without changing the mechanics of KEY_TYPE, CANONICAL_NAMESPACE, I think we could make this all easier to understand by just dropping the term “Locator”. Instead, we would just have abstract key classes and concrete key classes. Concrete classes would be ones that implement all methods and override all required class attributes, including but not limited to CANONICAL_NAMESPACE .

For example, in a fully-cleaned-up opaque-keys repo, we could have:

class OpaqueKey: KEY_TYPE: str # The entrypoint. Should be set exactly once per Key hierarchy. CANONICAL_NAMESPACE: str # The key prefix. If unspecified, class is abstract. class ContextKey(OpaqueKey): # formerly LearningContextKey KEY_TYPE: "context_key" ... # abstract! class CourselikeKey(ContextKey): # formerly CourseKey ... # abstract! class CourseRunKey(CourselikeKey): # formerly CourseLocator CANONICAL_NAMESPACE = "course-v1" ... class LegacyLibraryKey(CourselikeKey): # formerly LibraryLocator CANONICAL_NAMESPACE = "library-v1" ... class LibraryKey(ContextKey): # formerly LibraryLocatorV2 CANONICAL_NAMESPACE = "lib" ...