text: # -*- coding: utf-8 -*-
  # ------------------------------------------------------------------------------
  #
  #   Copyright 2018-2019 Fetch.AI Limited
  #
  #   Licensed under the Apache License, Version 2.0 (the "License");
  #   you may not use this file except in compliance with the License.
  #   You may obtain a copy of the License at
  #
  #       http://www.apache.org/licenses/LICENSE-2.0
  #
  #   Unless required by applicable law or agreed to in writing, software
  #   distributed under the License is distributed on an "AS IS" BASIS,
  #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  #   See the License for the specific language governing permissions and
  #   limitations under the License.
  #
  # ------------------------------------------------------------------------------
  """Base config data types."""
  import functools
  import re
  from abc import ABC, abstractmethod
  from enum import Enum
  from typing import (
      Any,
      Collection,
      Dict,
      Generic,
      List,
      Optional,
      Set,
      Tuple,
      TypeVar,
      Union,
      cast,
  )
  
  import semver
  from packaging.specifiers import SpecifierSet
  
  from aea.configurations.constants import (
      AGENT,
      CONNECTION,
      CONTRACT,
      DEFAULT_GIT_REF,
      PROTOCOL,
      SKILL,
  )
  from aea.exceptions import enforce
  from aea.helpers.base import (
      RegexConstrainedString,
      SIMPLE_ID_REGEX,
      SimpleId,
      SimpleIdOrStr,
  )
  
  
  T = TypeVar("T")
  
  
  VersionInfoClass = semver.VersionInfo
  PackageVersionLike = Union[str, semver.VersionInfo]
  
  
  class JSONSerializable(ABC):
      """Interface for JSON-serializable objects."""
  
      @property
      @abstractmethod
      def json(self) -> Dict:
          """Compute the JSON representation."""
  
      @classmethod
      def from_json(cls, obj: Dict) -> "JSONSerializable":
          """Build from a JSON object."""
  
  
  @functools.total_ordering
  class PackageVersion:
      """A package version."""
  
      _version: PackageVersionLike
  
      def __init__(self, version_like: PackageVersionLike) -> None:
          """
          Initialize a package version.
  
          :param version_like: a string, os a semver.VersionInfo object.
          """
          if isinstance(version_like, str) and version_like == "latest":
              self._version = version_like
          elif isinstance(version_like, str) and version_like == "any":
              self._version = version_like
          elif isinstance(version_like, str):
              self._version = VersionInfoClass.parse(version_like)
          elif isinstance(version_like, VersionInfoClass):
              self._version = version_like
          else:
              raise ValueError("Version type not valid.")
  
      @property
      def is_latest(self) -> bool:
          """Check whether the version is 'latest'."""
          return isinstance(self._version, str) and self._version == "latest"
  
      def __str__(self) -> str:
          """Get the string representation."""
          return str(self._version)
  
      def __eq__(self, other: Any) -> bool:
          """Check equality."""
          return isinstance(other, PackageVersion) and self._version == other._version
  
      def __lt__(self, other: Any) -> bool:
          """Compare with another object."""
          enforce(
              isinstance(other, PackageVersion),
              f"Cannot compare {type(self)} with type {type(other)}.",
          )
          other = cast(PackageVersion, other)
          if self.is_latest or other.is_latest:
              return self.is_latest < other.is_latest
          return str(self) < str(other)
  
  
  class PackageType(Enum):
      """Package types."""
  
      AGENT = AGENT
      PROTOCOL = PROTOCOL
      CONNECTION = CONNECTION
      CONTRACT = CONTRACT
      SKILL = SKILL
  
      def to_plural(self) -> str:
          """
          Get the plural name.
  
          >>> PackageType.AGENT.to_plural()
          'agents'
          >>> PackageType.PROTOCOL.to_plural()
          'protocols'
          >>> PackageType.CONNECTION.to_plural()
          'connections'
          >>> PackageType.SKILL.to_plural()
          'skills'
          >>> PackageType.CONTRACT.to_plural()
          'contracts'
  
          """
          return self.value + "s"
  
      def __str__(self) -> str:
          """Convert to string."""
          return str(self.value)
  
  
  class ComponentType(Enum):
      """Enum of component types supported."""
  
      PROTOCOL = PROTOCOL
      CONNECTION = CONNECTION
      SKILL = SKILL
      CONTRACT = CONTRACT
  
      def to_package_type(self) -> PackageType:
          """Get package type for component type."""
          return PackageType(self.value)
  
      @staticmethod
      def plurals() -> Collection[str]:  # pylint: disable=unsubscriptable-object
          """
          Get the collection of type names, plural.
  
          >>> ComponentType.plurals()
          ['protocols', 'connections', 'skills', 'contracts']
          """
          return list(map(lambda x: x.to_plural(), ComponentType))
  
      def to_plural(self) -> str:
          """
          Get the plural version of the component type.
  
          >>> ComponentType.PROTOCOL.to_plural()
          'protocols'
          >>> ComponentType.CONNECTION.to_plural()
          'connections'
          >>> ComponentType.SKILL.to_plural()
          'skills'
          >>> ComponentType.CONTRACT.to_plural()
          'contracts'
          """
          return self.value + "s"
  
      def __str__(self) -> str:
          """Get the string representation."""
          return str(self.value)
  
  
  class PublicId(JSONSerializable):
      """This class implement a public identifier.
  
      A public identifier is composed of three elements:
      - author
      - name
      - version
  
      The concatenation of those three elements gives the public identifier:
  
          author/name:version
  
      >>> public_id = PublicId("author", "my_package", "0.1.0")
      >>> assert public_id.author == "author"
      >>> assert public_id.name == "my_package"
      >>> assert public_id.version == "0.1.0"
      >>> another_public_id = PublicId("author", "my_package", "0.1.0")
      >>> assert hash(public_id) == hash(another_public_id)
      >>> assert public_id == another_public_id
      >>> latest_public_id = PublicId("author", "my_package", "latest")
      >>> latest_public_id
      <author/my_package:latest>
      >>> latest_public_id.package_version.is_latest
      True
      """
  
      __slots__ = ("_author", "_name", "_package_version")
  
      AUTHOR_REGEX = SIMPLE_ID_REGEX
      PACKAGE_NAME_REGEX = SIMPLE_ID_REGEX
      VERSION_NUMBER_PART_REGEX = r"(0|[1-9]\d*)"
      VERSION_REGEX = fr"(any|latest|({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})\.({VERSION_NUMBER_PART_REGEX})(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)"
      PUBLIC_ID_REGEX = fr"^({AUTHOR_REGEX})/({PACKAGE_NAME_REGEX})(:({VERSION_REGEX}))?$"
      PUBLIC_ID_URI_REGEX = (
          fr"^({AUTHOR_REGEX})/({PACKAGE_NAME_REGEX})/({VERSION_REGEX})$"
      )
  
      ANY_VERSION = "any"
      LATEST_VERSION = "latest"
  
      def __init__(
          self,
          author: SimpleIdOrStr,
          name: SimpleIdOrStr,
          version: Optional[PackageVersionLike] = None,
      ) -> None:
          """Initialize the public identifier."""
          self._author = SimpleId(author)
          self._name = SimpleId(name)
          self._package_version = (
              PackageVersion(version)
              if version is not None
              else PackageVersion(self.LATEST_VERSION)
          )
  
      @property
      def author(self) -> str:
          """Get the author."""
          return str(self._author)
  
      @property
      def name(self) -> str:
          """Get the name."""
          return str(self._name)
  
      @property
      def version(self) -> str:
          """Get the version string."""
          return str(self._package_version)
  
      @property
      def package_version(self) -> PackageVersion:
          """Get the package version object."""
          return self._package_version
  
      def to_any(self) -> "PublicId":
          """Return the same public id, but with any version."""
          return PublicId(self.author, self.name, self.ANY_VERSION)
  
      def same_prefix(self, other: "PublicId") -> bool:
          """Check if the other public id has the same author and name of this."""
          return self.name == other.name and self.author == other.author
  
      def to_latest(self) -> "PublicId":
          """Return the same public id, but with latest version."""
          return PublicId(self.author, self.name, self.LATEST_VERSION)
  
      @classmethod
      def is_valid_str(cls, public_id_string: str) -> bool:
          """
          Check if a string is a public id.
  
          :param public_id_string: the public id in string format.
          :return: bool indicating validity
          """
          match = re.match(cls.PUBLIC_ID_REGEX, public_id_string)
          return match is not None
  
      @classmethod
      def from_str(cls, public_id_string: str) -> "PublicId":
          """
          Initialize the public id from the string.
  
          >>> str(PublicId.from_str("author/package_name:0.1.0"))
          'author/package_name:0.1.0'
  
          A bad formatted input raises value error:
          >>> PublicId.from_str("bad/formatted:input")
          Traceback (most recent call last):
          ...
          ValueError: Input 'bad/formatted:input' is not well formatted.
  
          :param public_id_string: the public id in string format.
          :return: the public id object.
          :raises ValueError: if the string in input is not well formatted.
          """
          match = re.match(cls.PUBLIC_ID_REGEX, public_id_string)
          if match is None:
              raise ValueError(
                  "Input '{}' is not well formatted.".format(public_id_string)
              )
          username = match.group(1)
          package_name = match.group(2)
          version = match.group(3)[1:] if ":" in public_id_string else None
          return PublicId(username, package_name, version)
  
      @classmethod
      def try_from_str(cls, public_id_string: str) -> Optional["PublicId"]:
          """
          Safely try to get public id from string.
  
          :param public_id_string: the public id in string format.
          :return: the public id object or None
          """
          result: Optional[PublicId] = None
          try:
              result = cls.from_str(public_id_string)
          except ValueError:
              pass
          return result
  
      @classmethod
      def from_uri_path(cls, public_id_uri_path: str) -> "PublicId":
          """
          Initialize the public id from the string.
  
          >>> str(PublicId.from_uri_path("author/package_name/0.1.0"))
          'author/package_name:0.1.0'
  
          A bad formatted input raises value error:
          >>> PublicId.from_uri_path("bad/formatted:input")
          Traceback (most recent call last):
          ...
          ValueError: Input 'bad/formatted:input' is not well formatted.
  
          :param public_id_uri_path: the public id in uri path string format.
          :return: the public id object.
          :raises ValueError: if the string in input is not well formatted.
          """
          if not re.match(cls.PUBLIC_ID_URI_REGEX, public_id_uri_path):
              raise ValueError(
                  "Input '{}' is not well formatted.".format(public_id_uri_path)
              )
          username, package_name, version = re.findall(
              cls.PUBLIC_ID_URI_REGEX, public_id_uri_path
          )[0][:3]
          return PublicId(username, package_name, version)
  
      @property
      def to_uri_path(self) -> str:
          """
          Turn the public id into a uri path string.
  
          :return: uri path string
          """
          return "{author}/{name}/{version}".format(
              author=self.author, name=self.name, version=self.version
          )
  
      @property
      def json(self) -> Dict:
          """Compute the JSON representation."""
          return {"author": self.author, "name": self.name, "version": self.version}
  
      @classmethod
      def from_json(cls, obj: Dict) -> "PublicId":
          """Build from a JSON object."""
          return PublicId(obj["author"], obj["name"], obj["version"],)
  
      def __hash__(self) -> int:
          """Get the hash."""
          return hash((self.author, self.name, self.version))
  
      def __str__(self) -> str:
          """Get the string representation."""
          return "{author}/{name}:{version}".format(
              author=self.author, name=self.name, version=self.version
          )
  
      def __repr__(self) -> str:
          """Get the representation."""
          return f"<{self}>"
  
      def __eq__(self, other: Any) -> bool:
          """Compare with another object."""
          return (
              isinstance(other, PublicId)
              and self.author == other.author
              and self.name == other.name
              and self.version == other.version
          )
  
      def __lt__(self, other: Any) -> bool:
          """
          Compare two public ids.
  
          >>> public_id_1 = PublicId("author_1", "name_1", "0.1.0")
          >>> public_id_2 = PublicId("author_1", "name_1", "0.1.1")
          >>> public_id_3 = PublicId("author_1", "name_2", "0.1.0")
          >>> public_id_1 > public_id_2
          False
          >>> public_id_1 < public_id_2
          True
  
          >>> public_id_1 < public_id_3
          Traceback (most recent call last):
          ...
          ValueError: The public IDs author_1/name_1:0.1.0 and author_1/name_2:0.1.0 cannot be compared. Their author or name attributes are different.
  
          """
          if (
              isinstance(other, PublicId)
              and self.author == other.author
              and self.name == other.name
          ):
              return self.package_version < other.package_version
          raise ValueError(
              "The public IDs {} and {} cannot be compared. Their author or name attributes are different.".format(
                  self, other
              )
          )
  
  
  class PackageId:
      """A package identifier."""
  
      PACKAGE_TYPE_REGEX = r"({}|{}|{}|{}|{})".format(
          PackageType.AGENT,
          PackageType.PROTOCOL,
          PackageType.SKILL,
          PackageType.CONNECTION,
          PackageType.CONTRACT,
      )
      PACKAGE_ID_URI_REGEX = r"{}/{}".format(
          PACKAGE_TYPE_REGEX, PublicId.PUBLIC_ID_URI_REGEX[1:-1]
      )
  
      __slots__ = ("_package_type", "_public_id")
  
      def __init__(
          self, package_type: Union[PackageType, str], public_id: PublicId
      ) -> None:
          """
          Initialize the package id.
  
          :param package_type: the package type.
          :param public_id: the public id.
          """
          self._package_type = PackageType(package_type)
          self._public_id = public_id
  
      @property
      def package_type(self) -> PackageType:
          """Get the package type."""
          return self._package_type
  
      @property
      def public_id(self) -> PublicId:
          """Get the public id."""
          return self._public_id
  
      @property
      def author(self) -> str:
          """Get the author of the package."""
          return self.public_id.author
  
      @property
      def name(self) -> str:
          """Get the name of the package."""
          return self.public_id.name
  
      @property
      def version(self) -> str:
          """Get the version of the package."""
          return self.public_id.version
  
      @property
      def package_prefix(self) -> Tuple[PackageType, str, str]:
          """Get the package identifier without the version."""
          return self.package_type, self.author, self.name
  
      @classmethod
      def from_uri_path(cls, package_id_uri_path: str) -> "PackageId":
          """
          Initialize the package id from the string.
  
          >>> str(PackageId.from_uri_path("skill/author/package_name/0.1.0"))
          '(skill, author/package_name:0.1.0)'
  
          A bad formatted input raises value error:
          >>> PackageId.from_uri_path("very/bad/formatted:input")
          Traceback (most recent call last):
          ...
          ValueError: Input 'very/bad/formatted:input' is not well formatted.
  
          :param package_id_uri_path: the package id in uri path string format.
          :return: the package id object.
          :raises ValueError: if the string in input is not well formatted.
          """
          if not re.match(cls.PACKAGE_ID_URI_REGEX, package_id_uri_path):
              raise ValueError(
                  "Input '{}' is not well formatted.".format(package_id_uri_path)
              )
          package_type_str, username, package_name, version = re.findall(
              cls.PACKAGE_ID_URI_REGEX, package_id_uri_path
          )[0][:4]
          package_type = PackageType(package_type_str)
          public_id = PublicId(username, package_name, version)
          return PackageId(package_type, public_id)
  
      @property
      def to_uri_path(self) -> str:
          """
          Turn the package id into a uri path string.
  
          :return: uri path string
          """
          return f"{str(self.package_type)}/{self.author}/{self.name}/{self.version}"
  
      def __hash__(self) -> int:
          """Get the hash."""
          return hash((self.package_type, self.public_id))
  
      def __str__(self) -> str:
          """Get the string representation."""
          return "({package_type}, {public_id})".format(
              package_type=self.package_type.value, public_id=self.public_id,
          )
  
      def __repr__(self) -> str:
          """Get the object representation in string."""
          return f"PackageId{self.__str__()}"
  
      def __eq__(self, other: Any) -> bool:
          """Compare with another object."""
          return (
              isinstance(other, PackageId)
              and self.package_type == other.package_type
              and self.public_id == other.public_id
          )
  
      def __lt__(self, other: Any) -> bool:
          """Compare two public ids."""
          return str(self) < str(other)
  
  
  class ComponentId(PackageId):
      """
      Class to represent a component identifier.
  
      A component id is a package id, but excludes the case when the package is an agent.
      >>> pacakge_id = PackageId(PackageType.PROTOCOL, PublicId("author", "name", "0.1.0"))
      >>> component_id = ComponentId(ComponentType.PROTOCOL, PublicId("author", "name", "0.1.0"))
      >>> pacakge_id == component_id
      True
  
      >>> component_id2 = ComponentId(ComponentType.PROTOCOL, PublicId("author", "name", "0.1.1"))
      >>> pacakge_id == component_id2
      False
      """
  
      def __init__(
          self, component_type: Union[ComponentType, str], public_id: PublicId
      ) -> None:
          """
          Initialize the component id.
  
          :param component_type: the component type.
          :param public_id: the public id.
          """
          component_type = ComponentType(component_type)
          super().__init__(component_type.to_package_type(), public_id)
  
      @property
      def component_type(self) -> ComponentType:
          """Get the component type."""
          return ComponentType(self.package_type.value)
  
      @property
      def component_prefix(self) -> Tuple[ComponentType, str, str]:
          """Get the component identifier without the version."""
          package_prefix = super().package_prefix
          package_type, author, name = package_prefix
          return ComponentType(package_type.value), author, name
  
      def same_prefix(self, other: "ComponentId") -> bool:
          """Check if the other component id has the same type, author and name of this."""
          return (
              self.component_type == other.component_type
              and self.public_id.same_prefix(other.public_id)
          )
  
      @property
      def prefix_import_path(self) -> str:
          """Get the prefix import path for this component."""
          return "packages.{}.{}.{}".format(
              self.public_id.author, self.component_type.to_plural(), self.public_id.name
          )
  
      @property
      def json(self) -> Dict:
          """Get the JSON representation."""
          return dict(**self.public_id.json, type=str(self.component_type))
  
      @classmethod
      def from_json(cls, json_data: Dict) -> "ComponentId":
          """Create  component id from json data."""
          return cls(
              component_type=json_data["type"], public_id=PublicId.from_json(json_data)
          )
  
  
  class PyPIPackageName(RegexConstrainedString):
      """A PyPI Package name."""
  
      REGEX = re.compile(r"^([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$")
  
  
  class GitRef(RegexConstrainedString):
      """
      A Git reference.
  
      It can be a branch name, a commit hash or a tag.
      """
  
      REGEX = re.compile(r"^[A-Za-z0-9/.\-_]+$")
  
  
  class Dependency:
      """
      This class represents a PyPI dependency.
  
      It contains the following information:
      - version: a version specifier(s) (e.g. '==0.1.0').
      - index: the PyPI index where to download the package from (default: https://pypi.org)
      - git: the URL to the Git repository (e.g. https://github.com/fetchai/agents-aea.git)
      - ref: either the branch name, the tag, the commit number or a Git reference (default: 'master'.)
  
      If the 'git' field is set, the 'version' field will be ignored.
      These fields will be forwarded to the 'pip' command.
      """
  
      __slots__ = ("_name", "_version", "_index", "_git", "_ref")
  
      def __init__(
          self,
          name: Union[PyPIPackageName, str],
          version: Union[str, SpecifierSet] = "",
          index: Optional[str] = None,
          git: Optional[str] = None,
          ref: Optional[Union[GitRef, str]] = None,
      ) -> None:
          """
          Initialize a PyPI dependency.
  
          :param name: the package name.
          :param version: the specifier set object
          :param index: the URL to the PyPI server.
          :param git: the URL to a git repository.
          :param ref: the Git reference (branch/commit/tag).
          """
          self._name: PyPIPackageName = PyPIPackageName(name)
          self._version: SpecifierSet = self._parse_version(version)
          self._index: Optional[str] = index
          self._git: Optional[str] = git
          self._ref: Optional[GitRef] = GitRef(ref) if ref is not None else None
  
      @property
      def name(self) -> str:
          """Get the name."""
          return str(self._name)
  
      @property
      def version(self) -> str:
          """Get the version."""
          return str(self._version)
  
      @property
      def index(self) -> Optional[str]:
          """Get the index."""
          return str(self._index) if self._index else None
  
      @property
      def git(self) -> Optional[str]:
          """Get the git."""
          return str(self._git) if self._git else None
  
      @property
      def ref(self) -> Optional[str]:
          """Get the ref."""
          return str(self._ref) if self._ref else None
  
      @staticmethod
      def _parse_version(version: Union[str, SpecifierSet]) -> SpecifierSet:
          """
          Parse a version specifier set.
  
          :param version: the version, a string or a SpecifierSet instance.
          :return: the SpecifierSet instance.
          """
          return version if isinstance(version, SpecifierSet) else SpecifierSet(version)
  
      @classmethod
      def from_json(cls, obj: Dict[str, Dict[str, str]]) -> "Dependency":
          """Parse a dependency object from a dictionary."""
          if len(obj) != 1:
              raise ValueError(f"Only one key allowed, found {set(obj.keys())}")
          name, attributes = list(obj.items())[0]
          allowed_keys = {"version", "index", "git", "ref"}
          not_allowed_keys = set(attributes.keys()).difference(allowed_keys)
          if len(not_allowed_keys) > 0:
              raise ValueError(f"Not allowed keys: {not_allowed_keys}")
  
          version = attributes.get("version", "")
          index = attributes.get("index", None)
          git = attributes.get("git", None)
          ref = attributes.get("ref", None)
  
          return Dependency(name=name, version=version, index=index, git=git, ref=ref)
  
      def to_json(self) -> Dict[str, Dict[str, str]]:
          """Transform the object to JSON."""
          result = {}
          if self.version != "":
              result["version"] = self.version
          if self.index is not None:
              result["index"] = self.index
          if self.git is not None:
              result["git"] = cast(str, self.git)
          if self.ref is not None:
              result["ref"] = cast(str, self.ref)
          return {self.name: result}
  
      def get_pip_install_args(self) -> List[str]:
          """Get 'pip install' arguments."""
          name = self.name
          index = self.index
          git_url = self.git
          revision = self.ref if self.ref is not None else DEFAULT_GIT_REF
          version_constraint = str(self.version)
          command: List[str] = []
          if index is not None:
              command += ["-i", index]
          if git_url is not None:
              command += ["git+" + git_url + "@" + revision + "#egg=" + name]
          else:
              command += [name + version_constraint]
          return command
  
      def __str__(self) -> str:
          """Get the string representation."""
          return f"{self.__class__.__name__}(name='{self.name}', version='{self.version}', index='{self.index}', git='{self.git}', ref='{self.ref}')"
  
      def __eq__(self, other: Any) -> bool:
          """Compare with another object."""
          return (
              isinstance(other, Dependency)
              and self._name == other._name
              and self._version == other._version
              and self._index == other._index
              and self._git == other._git
              and self._ref == other._ref
          )
  
  
  Dependencies = Dict[str, Dependency]
  """
  A dictionary from package name to dependency data structure (see above).
  The package name must satisfy  <a href="https://www.python.org/dev/peps/pep-0426/#name">the constraints on Python packages names</a>.
  
  The main advantage of having a dictionary is that we implicitly filter out dependency duplicates.
  We cannot have two items with the same package name since the keys of a YAML object form a set.
  """
  
  
  class CRUDCollection(Generic[T]):
      """Interface of a CRUD collection."""
  
      __slots__ = ("_items_by_id",)
  
      def __init__(self) -> None:
          """Instantiate a CRUD collection."""
          self._items_by_id = {}  # type: Dict[str, T]
  
      def create(self, item_id: str, item: T) -> None:
          """
          Add an item.
  
          :param item_id: the item id.
          :param item: the item to be added.
          :return: None
          :raises ValueError: if the item with the same id is already in the collection.
          """
          if item_id in self._items_by_id:
              raise ValueError("Item with name {} already present!".format(item_id))
          self._items_by_id[item_id] = item
  
      def read(self, item_id: str) -> Optional[T]:
          """
          Get an item by its name.
  
          :param item_id: the item id.
          :return: the associated item, or None if the item id is not present.
          """
          return self._items_by_id.get(item_id, None)
  
      def update(self, item_id: str, item: T) -> None:
          """
          Update an existing item.
  
          :param item_id: the item id.
          :param item: the item to be added.
          :return: None
          """
          self._items_by_id[item_id] = item
  
      def delete(self, item_id: str) -> None:
          """Delete an item."""
          if item_id in self._items_by_id.keys():
              del self._items_by_id[item_id]
  
      def read_all(self) -> List[Tuple[str, T]]:
          """Read all the items."""
          return [  # pylint: disable=unnecessary-comprehension
              (k, v) for k, v in self._items_by_id.items()
          ]
  
      def keys(self) -> Set[str]:
          """Get the set of keys."""
          return set(self._items_by_id.keys())
  