camelot_key API documentation
Description: Python library to parse and format musical key notation and convert between Camelot key notation

# Package `camelot_key`

Python library to parse and format musical key notation and convert between Camelot key notation

## Global variables

`var ALTERNATE_KEYS`

  Alternate key mapping, keys that can be represented differently but are the same note

`var CAMELOT_COLORS`

  Colors representing camelot key pairs

`var CAMELOT_TO_MUSICAL`

  Mapping of camelot key number/letter pairs to musical key

`var MUSICAL_LONG`

  'Long' format for short musical key representations, can be passed to `Key`

`var MUSICAL_TO_CAMELOT`

  Mapping of musical key to camelot key number/letter pairs (inverse of `CAMELOT_TO_MUSICAL`)

## Functions

`def parse(value: str | Dict[str, Any]) ‑> Key | None`
  Expand source code

      def parse(value: t.Union[str, t.Dict[str, t.Any]]) -> t.Optional[Key]:
          """\
          Parse a value representing a musical key into a Key instance, if possible.
          Automatically determines the format and parses the data, returning None if a
          key could not be determined.  Input formats may include:

          **Beatport API - musical key**
          ```
          {
              'letter': 'C',
              'is_sharp': True,
              'is_flat': False,
              'chord_type': {'name': 'minor'},
          }
          ```

          **Beatport API - camelot key**
          ```
          {
              'camelot_number': 12,
              'camelot_letter': 'B',
          }
          ```

          **Musical Key - string:** `C#m`, `C # Minor`, etc

          **Camelot Key - string:** `12B`
          """

          if isinstance(value, str):
              res = _parse_str__musical(value)
              if res is not None:
                  return res

              res = _parse_str__camelot(value)
              if res is not None:
                  return res
          else:
              res = _parse_beatport__musical(value)
              if res is not None:
                  return res

              res = _parse_beatport__camelot(value)
              if res is not None:
                  return res

  Parse a value representing a musical key into a Key instance, if possible. Automatically determines the format and parses the data, returning None if a key could not be determined. Input formats may include:

  Beatport API - musical key

      {
          'letter': 'C',
          'is_sharp': True,
          'is_flat': False,
          'chord_type': {'name': 'minor'},
      }

  Beatport API - camelot key

      {
          'camelot_number': 12,
          'camelot_letter': 'B',
      }

  Musical Key - string: `C#m`, `C # Minor`, etc

  Camelot Key - string: `12B`

## Classes

`class Key (letter: Literal['A', 'B', 'C', 'D', 'E', 'F', 'G'] = None,
sharp: bool = False,
flat: bool = False,
type: Literal['major', 'minor'] = 'major',
key: str = None,
camelot_number: Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] | None = None,
camelot_letter: Literal['A', 'B'] | None = None,
camelot_color: str | None = None)`
  Expand source code

      @dataclass(frozen=True)
      class Key:
          """\
          Represents a musical/camelot key.  Keys are immutable and hashable, and can
          be compared and sorted.
          """

          letter: t.Literal['A', 'B', 'C', 'D', 'E', 'F', 'G'] = None
          """Musical key letter"""
          sharp: bool = False
          """Whether the key is sharp"""
          flat: bool = False
          """Whether the key is flat"""
          type: t.Literal['major', 'minor'] = 'major'
          """Chord type - major or minor"""
          key: str = None
          """'Short' representation of the musical key, like 'F#m'"""
          camelot_number: t.Optional[t.Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]] = None
          """Number for the camelot key"""
          camelot_letter: t.Optional[t.Literal['A', 'B']] = None
          """Letter for the camelot key"""
          camelot_color: t.Optional[str] = None
          """Color to use for displaying the camelot key"""

          def __post_init__parse(self):
              """\
              On init, ensure we have musical key information.  If it was provided
              then we're fine; otherwise attempt to get it from the camelot key.  If
              no camelot key was provided in that case, it's an error.
              """

              if self.letter is None:
                  if self.camelot_number is None or self.camelot_letter is None:
                      raise ValueError("No key information provided")
                  short_key = CAMELOT_TO_MUSICAL.get((self.camelot_number, self.camelot_letter))
                  if not short_key:
                      raise ValueError("No valid key information provided")
                  long_key = MUSICAL_LONG.get(short_key)
                  if not long_key:
                      raise ValueError("No valid key information provided")
                  for k, v in long_key.items():
                      super().__setattr__(k, v)

              if self.sharp and self.flat:
                  super().__setattr__('flat', False)

          def __post_init__key(self):
              """\
              On init, generate the short key representation/string representation
              from the musical key information.
              """

              if self.key is None:
                  parts = [self.letter]

                  if self.sharp:
                      parts.append('#')
                  elif self.flat:
                      parts.append('b')

                  if self.type == 'major' or not self.type:
                      parts.append('M')
                  elif self.type == 'minor':
                      parts.append('m')

                  super().__setattr__('key', ''.join(parts))

          def __post_init__camelot(self):
              """\
              On init, generate the camelot key information from the musical key
              information if possible.
              """

              if self.camelot_number is None or self.camelot_letter is None:
                  for alt_key in self.alternate_keys:
                      res = MUSICAL_TO_CAMELOT.get(alt_key.key)
                      if res:
                          n, l = res
                          super().__setattr__('camelot_number', n)
                          super().__setattr__('camelot_letter', l)
                          break
              if self.camelot_color is None and self.camelot_number and self.camelot_letter:
                  c = CAMELOT_COLORS.get(self.camelot_number, {}).get(self.camelot_letter)
                  super().__setattr__('camelot_color', c)

          def __post_init__validate(self):
              """\
              On init, validate that the data in the class instance is actually valid.
              In some cases an exception might have been thrown earlier.
              """

              if self.letter is None:
                  raise TypeError("letter must be present")
              if not isinstance(self.letter, str):
                  raise TypeError("letter must be a string")
              if self.letter not in ('A', 'B', 'C', 'D', 'E', 'F', 'G'):
                  raise ValueError("letter must be in A-G")

              if self.sharp not in (True, False):
                  raise TypeError("sharp must be True or False")
              if self.flat not in (True, False):
                  raise TypeError("flat must be True or False")
              if self.sharp and self.flat:
                  raise ValueError("Only one of sharp or flat may be True")

              if self.type is None:
                  raise TypeError("type must be present")
              if not isinstance(self.type, str):
                  raise TypeError("type must be a string")
              if self.type not in ('major', 'minor'):
                  raise ValueError("type must be major or minor")

              if self.key is None:
                  raise TypeError("key must be present")
              if not isinstance(self.key, str):
                  raise TypeError("key must be a string")

              if self.camelot_number or self.camelot_letter:
                  if self.camelot_number is None or self.camelot_letter is None:
                      raise ValueError("Both camelot_number and camelot_letter must be provided together")
                  if not isinstance(self.camelot_number, int):
                      raise TypeError("camelot_number must be an int")
                  if self.camelot_number < 1 or self.camelot_number > 12:
                      raise ValueError("camelot_number must be in 1-12")
                  if not isinstance(self.camelot_letter, str):
                      raise TypeError("camelot_letter must be a string")
                  if self.camelot_letter not in ('A', 'B'):
                      raise ValueError("camelot_letter must be A or B")

          def __post_init__(self):
              """\
              Run all post init tasks.  This ensures the resulting object has all of
              the data that we can determine from the given arguments, or raises an
              error if (somewhat) invalid data was provided.
              """

              self.__post_init__parse()
              self.__post_init__key()
              self.__post_init__camelot()
              self.__post_init__validate()

          @property
          def camelot_key(self) -> t.Optional[str]:
              """\
              Convert the camelot number and letter to a string representation, if
              both the number and letter are present.  If either are not present,
              returns None
              """

              if self.camelot_number and self.camelot_letter:
                  return f'{self.camelot_number}{self.camelot_letter}'

          def __str__(self) -> str:
              """\
              String representation of the key, returns the short key and the camelot
              key if available.
              """

              out = self.key
              ck = self.camelot_key
              if ck:
                  out = f'{out} ({ck})'
              return out

          def __hash__(self):
              """\
              Hash that is used to compare/uniquely identify a key.

              Note that two different keys representing the same note (i.e. F#/Gb) do
              not produce the same hash value; the properties must be the same
              """

              return hash((self.letter, self.sharp, self.flat, self.type))

          def __eq__(self, other: t.Self) -> bool:
              """\
              Compare two keys to see if they are the same.  Generally, two keys are
              the same if the musical key properties (letter, sharp/flat, type) are
              the same, however two keys representing the same note (i.e. F#/Gb) are
              also considered to be the same if the chord type is the same.
              """

              if isinstance(other, Key):
                  for other_alt in other.alternate_keys:
                      if hash(self) == hash(other_alt):
                          return True
              return False

          def __ne__(self, other: t.Self) -> bool:
              return not self.__eq__(other)

          @property
          def _camelot_key_number_repr(self) -> float:
              """\
              Return a numerical representation of the camelot key, for internal
              usage.  This numerical representation is sortable, in particular
              ensuring that the numbers sort in order, as do the letters (ex. 1A < 1B,
              1B < 2A, etc).

              For keys that do not have a camelot key, they are sorted first.

              The use case here is primarily implementing comparison outside of'
              equality, for sorting.
              """

              out = -1.0
              if self.camelot_number and self.camelot_letter:
                  out = float(self.camelot_number)
                  if self.camelot_letter == 'B':
                      out += 0.5
              return out

          def __lt__(self, other: t.Self) -> bool:
              if not isinstance(other, Key):
                  raise ValueError("Uncomparable types")
              return self._camelot_key_number_repr < other._camelot_key_number_repr

          def __le__(self, other: t.Self) -> bool:
              if not isinstance(other, Key):
                  raise ValueError("Uncomparable types")
              return self._camelot_key_number_repr <= other._camelot_key_number_repr

          def __gt__(self, other: t.Self) -> bool:
              if not isinstance(other, Key):
                  raise ValueError("Uncomparable types")
              return self._camelot_key_number_repr > other._camelot_key_number_repr

          def __ge__(self, other: t.Self) -> bool:
              if not isinstance(other, Key):
                  raise ValueError("Uncomparable types")
              return self._camelot_key_number_repr >= other._camelot_key_number_repr

          @property
          def alternate_keys(self) -> t.Generator[t.Self, None, None]:
              """\
              Produce alternate keys that are technically the same key as this one.
              The current object is always produced as it is always the same, and if
              another key that is the same exists it is also produced.

              Examples:
                * F# is also Gb
                * C# is also Db
                * E# is also F (uncommon but not technically wrong...)
              """

              yield self
              alt = ALTERNATE_KEYS.get((self.letter, self.sharp, self.flat))
              if alt:
                  yield self.__class__(**alt, type=self.type)

          def _copy(self, **kwargs) -> t.Self:
              """\
              Create a copy of the current object with updated properties from kwargs.
              If kwargs contains camelot key information, the musical key information
              is discarded and only camelot key is used to construct the new instance,
              and vice versa - if musical key information is provided, camelot key
              information is discarded.

              If no kwargs are given, the musical key information is used.
              """

              camelot_args = ('camelot_number', 'camelot_letter')
              musical_args = ('letter', 'sharp', 'flat', 'type')

              new_args = {
                  'letter': self.letter,
                  'sharp': self.sharp,
                  'flat': self.flat,
                  'type': self.type,
                  'camelot_number': self.camelot_number,
                  'camelot_letter': self.camelot_letter,
              }

              if any(((k in kwargs) for k in camelot_args)):
                  for k in musical_args:
                      del new_args[k]
              else:
                  for k in camelot_args:
                      del new_args[k]
              new_args.update(kwargs)
              return Key(**new_args)

          def shift(self, n: int):
              """\
              Shift the key by N steps on the camelot wheel/circle of fifths,
              returning a new Key object.  If the current instance does not have
              camelot key information, raises an error.

              .. important:: Note that this is NOT equivalent to a key shift such as you may find in DJ software - where key shifting is done by note (for example shifing CM up by 1 results in C#M), but rather by the adjacent key on the circle of fifths (shifting CM up by 1 results in FM).

              .. tip:: To shift by adjacent notes, use a factor of 5.
              """

              if not (self.camelot_number and self.camelot_letter):
                  raise ValueError("Missing camelot key information")

              return self._copy(camelot_number=((self.camelot_number + n) % 12) or 12)

          def get_adjacent_keys(self, n: int=1) -> t.Generator[t.Tuple[int, t.Self], None, None]:
              """\
              Get keys adjacent to this key on the circle of fifths, along with the
              number of steps by which that key must be shifted to be adjacent.  When
              n>1 is passed, consider keys that are not immediately adjacent.  If n=1,
              only immediately adjacent keys are considered and the number of steps to
              shift is always 0.

              In terms of the camelot wheel, for example:
              Given a key 3A, adjacent keys are 3A (same), 3B (same, but major instead
              of minor scale), 2A, and 4A.  For all of these the shift value is 0.
              With n=2, additional adjacent keys would include:

                * 3A +/- 5: (-1, 8A), (+1, 10A)
                * 3B +/- 5: (-1, 8B), (+1, 10B)
                * 4A +/- 5: (-1, 9A), (+1, 11A)
                * 2A +/- 5: (-1, 7A), (+1, 9A)

              Generated values are a tuple of (shift, key)

              .. note:: Nothing is generated if this instance does not have camelot key information
              .. note:: The results are not in any particular order (there is a deterministic order but it does not have any meaning) but are sortable; sorting without any key results in a list sorted by shift distance, then camelot number & letter
              """

              if self.camelot_number and self.camelot_letter:
                  this_key = self
                  this_alt_key = self._copy(camelot_letter='B' if self.camelot_letter == 'A' else 'A')
                  this_adj_prev = self.shift(-1)
                  this_adj_next = self.shift(1)
                  # This key
                  yield 0, this_key
                  # Alternate key - major vs. minor
                  yield 0, this_alt_key
                  # Previous # on the camelot wheel, same scale
                  yield 0, this_adj_prev
                  # Next # on the camelot wheel, same scale
                  yield 0, this_adj_next

                  for i in range(2, n + 1):
                      diff = i - 1
                      shift = diff * 5
                      # Produce the alternate key first - when shifted by N these keys will be exactly the alternate key
                      yield -diff, this_alt_key.shift(shift)
                      yield diff, this_alt_key.shift(-shift)
                      # Then keys that when shifted become the current key, or an ajacent key
                      yield -diff, this_key.shift(shift)
                      yield diff, this_key.shift(-shift)
                      yield -diff, this_adj_prev.shift(shift)
                      yield diff, this_adj_prev.shift(-shift)
                      yield -diff, this_adj_next.shift(shift)
                      yield diff, this_adj_next.shift(-shift)

  Represents a musical/camelot key. Keys are immutable and hashable, and can be compared and sorted.

  ### Instance variables

  `prop alternate_keys : Generator[Self, None, None]`
    Expand source code

        @property
        def alternate_keys(self) -> t.Generator[t.Self, None, None]:
            """\
            Produce alternate keys that are technically the same key as this one.
            The current object is always produced as it is always the same, and if
            another key that is the same exists it is also produced.

            Examples:
              * F# is also Gb
              * C# is also Db
              * E# is also F (uncommon but not technically wrong...)
            """

            yield self
            alt = ALTERNATE_KEYS.get((self.letter, self.sharp, self.flat))
            if alt:
                yield self.__class__(**alt, type=self.type)

    Produce alternate keys that are technically the same key as this one. The current object is always produced as it is always the same, and if another key that is the same exists it is also produced.

    ## Examples

    - F# is also Gb
    - C# is also Db
    - E# is also F (uncommon but not technically wrong…)
  `var camelot_color : str | None`

    Color to use for displaying the camelot key

  `prop camelot_key : str | None`
    Expand source code

        @property
        def camelot_key(self) -> t.Optional[str]:
            """\
            Convert the camelot number and letter to a string representation, if
            both the number and letter are present.  If either are not present,
            returns None
            """

            if self.camelot_number and self.camelot_letter:
                return f'{self.camelot_number}{self.camelot_letter}'

    Convert the camelot number and letter to a string representation, if both the number and letter are present. If either are not present, returns None

  `var camelot_letter : Literal['A', 'B'] | None`

    Letter for the camelot key

  `var camelot_number : Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] | None`

    Number for the camelot key

  `var flat : bool`

    Whether the key is flat

  `var key : str`

    'Short' representation of the musical key, like 'F#m'

  `var letter : Literal['A', 'B', 'C', 'D', 'E', 'F', 'G']`

    Musical key letter

  `var sharp : bool`

    Whether the key is sharp

  `var type : Literal['major', 'minor']`

    Chord type - major or minor

  ### Methods

  `def get_adjacent_keys(self, n: int = 1) ‑> Generator[Tuple[int, Self], None, None]`
    Expand source code

        def get_adjacent_keys(self, n: int=1) -> t.Generator[t.Tuple[int, t.Self], None, None]:
            """\
            Get keys adjacent to this key on the circle of fifths, along with the
            number of steps by which that key must be shifted to be adjacent.  When
            n>1 is passed, consider keys that are not immediately adjacent.  If n=1,
            only immediately adjacent keys are considered and the number of steps to
            shift is always 0.

            In terms of the camelot wheel, for example:
            Given a key 3A, adjacent keys are 3A (same), 3B (same, but major instead
            of minor scale), 2A, and 4A.  For all of these the shift value is 0.
            With n=2, additional adjacent keys would include:

              * 3A +/- 5: (-1, 8A), (+1, 10A)
              * 3B +/- 5: (-1, 8B), (+1, 10B)
              * 4A +/- 5: (-1, 9A), (+1, 11A)
              * 2A +/- 5: (-1, 7A), (+1, 9A)

            Generated values are a tuple of (shift, key)

            .. note:: Nothing is generated if this instance does not have camelot key information
            .. note:: The results are not in any particular order (there is a deterministic order but it does not have any meaning) but are sortable; sorting without any key results in a list sorted by shift distance, then camelot number & letter
            """

            if self.camelot_number and self.camelot_letter:
                this_key = self
                this_alt_key = self._copy(camelot_letter='B' if self.camelot_letter == 'A' else 'A')
                this_adj_prev = self.shift(-1)
                this_adj_next = self.shift(1)
                # This key
                yield 0, this_key
                # Alternate key - major vs. minor
                yield 0, this_alt_key
                # Previous # on the camelot wheel, same scale
                yield 0, this_adj_prev
                # Next # on the camelot wheel, same scale
                yield 0, this_adj_next

                for i in range(2, n + 1):
                    diff = i - 1
                    shift = diff * 5
                    # Produce the alternate key first - when shifted by N these keys will be exactly the alternate key
                    yield -diff, this_alt_key.shift(shift)
                    yield diff, this_alt_key.shift(-shift)
                    # Then keys that when shifted become the current key, or an ajacent key
                    yield -diff, this_key.shift(shift)
                    yield diff, this_key.shift(-shift)
                    yield -diff, this_adj_prev.shift(shift)
                    yield diff, this_adj_prev.shift(-shift)
                    yield -diff, this_adj_next.shift(shift)
                    yield diff, this_adj_next.shift(-shift)

    Get keys adjacent to this key on the circle of fifths, along with the number of steps by which that key must be shifted to be adjacent. When n>1 is passed, consider keys that are not immediately adjacent. If n=1, only immediately adjacent keys are considered and the number of steps to shift is always 0.

    In terms of the camelot wheel, for example: Given a key 3A, adjacent keys are 3A (same), 3B (same, but major instead of minor scale), 2A, and 4A. For all of these the shift value is 0. With n=2, additional adjacent keys would include:

    - 3A +/- 5: (-1, 8A), (+1, 10A)
    - 3B +/- 5: (-1, 8B), (+1, 10B)
    - 4A +/- 5: (-1, 9A), (+1, 11A)
    - 2A +/- 5: (-1, 7A), (+1, 9A)

    Generated values are a tuple of (shift, key)

    Note: Nothing is generated if this instance does not have camelot key information

    Note: The results are not in any particular order (there is a deterministic order but it does not have any meaning) but are sortable; sorting without any key results in a list sorted by shift distance, then camelot number & letter

  `def shift(self, n: int)`
    Expand source code

        def shift(self, n: int):
            """\
            Shift the key by N steps on the camelot wheel/circle of fifths,
            returning a new Key object.  If the current instance does not have
            camelot key information, raises an error.

            .. important:: Note that this is NOT equivalent to a key shift such as you may find in DJ software - where key shifting is done by note (for example shifing CM up by 1 results in C#M), but rather by the adjacent key on the circle of fifths (shifting CM up by 1 results in FM).

            .. tip:: To shift by adjacent notes, use a factor of 5.
            """

            if not (self.camelot_number and self.camelot_letter):
                raise ValueError("Missing camelot key information")

            return self._copy(camelot_number=((self.camelot_number + n) % 12) or 12)

    Shift the key by N steps on the camelot wheel/circle of fifths, returning a new Key object. If the current instance does not have camelot key information, raises an error.

    Important: Note that this is NOT equivalent to a key shift such as you may find in DJ software - where key shifting is done by note (for example shifing CM up by 1 results in C#M), but rather by the adjacent key on the circle of fifths (shifting CM up by 1 results in FM).

    Tip: To shift by adjacent notes, use a factor of 5.