text: from collections import UserList
  from fnmatch import fnmatch
  from typing import Optional
  import copy
  from astropy.time import Time
  import pathlib
  from urllib.parse import urlparse
  import os
  
  from traitlets import (
      Bool,
      CaselessStrEnum,
      Dict,
      Enum,
      Float,
      Int,
      Integer,
      List,
      Long,
      TraitError,
      TraitType,
      Unicode,
      observe,
      Set,
      CRegExp,
  )
  from traitlets.config import boolean_flag as flag
  
  from .component import non_abstract_children
  
  __all__ = [
      "Path",
      "Int",
      "Integer",
      "Float",
      "Unicode",
      "Enum",
      "Long",
      "List",
      "Bool",
      "Set",
      "CRegExp",
      "Dict",
      "flag",
      "TraitError",
      "observe",
      "CaselessStrEnum",
      "create_class_enum_trait",
      "classes_with_traits",
      "has_traits",
      "TelescopeParameter",
      "FloatTelescopeParameter",
      "IntTelescopeParameter",
      "AstroTime",
  ]
  
  import logging
  
  logger = logging.getLogger(__name__)
  
  
  class AstroTime(TraitType):
      """ A trait representing a point in Time, as understood by `astropy.time`"""
  
      def validate(self, obj, value):
          """ try to parse and return an ISO time string """
          try:
              the_time = Time(value)
              the_time.format = "iso"
              return the_time
          except ValueError:
              return self.error(obj, value)
  
      def info(self):
          info = "an ISO8601 datestring or Time instance"
          if self.allow_none:
              info += "or None"
          return info
  
  
  class Path(TraitType):
      """
      A path Trait for input/output files.
  
      Parameters
      ----------
      exists: boolean or None
          If True, path must exist, if False path must not exist
  
      directory_ok: boolean
          If False, path must not be a directory
      file_ok: boolean
          If False, path must not be a file
      """
  
      def __init__(self, *args, exists=None, directory_ok=True, file_ok=True, **kwargs):
          default_value = kwargs.pop("default_value", None)
  
          super().__init__(*args, default_value=default_value, allow_none=True, **kwargs)
          self.exists = exists
          self.directory_ok = directory_ok
          self.file_ok = file_ok
  
      def info(self):
          info = "a pathlib.Path or non-empty str for "
          if self.exists is True:
              info += "an existing"
          elif self.exists is False:
              info += "a not existing"
          else:
              info += "a"
  
          if self.directory_ok and self.file_ok:
              info += " directory or file"
          else:
              if self.file_ok:
                  info += " file"
              if self.directory_ok:
                  info += "directory"
          if self.allow_none:
              info += " or None"
  
          return info
  
      def validate(self, obj, value):
          if isinstance(value, bytes):
              value = os.fsdecode(value)
  
          if not isinstance(value, (str, pathlib.Path)):
              return self.error(obj, value)
  
          if isinstance(value, str):
              if value == "":
                  return self.error(obj, value)
  
              try:
                  url = urlparse(value)
              except ValueError:
                  return self.error(obj, value)
  
              if url.scheme not in ("", "file"):
                  return self.error(obj, value)
  
              value = pathlib.Path(url.netloc, url.path)
  
          value = value.absolute()
          exists = value.exists()
          if self.exists is not None:
              if exists != self.exists:
                  raise TraitError(
                      'Path "{}" {} exist'.format(
                          value, "does not" if self.exists else "must not"
                      )
                  )
          if exists:
              if not self.directory_ok and value.is_dir():
                  raise TraitError(f'Path "{value}" must not be a directory')
              if not self.file_ok and value.is_file():
                  raise TraitError(f'Path "{value}" must not be a file')
  
          return value
  
  
  def create_class_enum_trait(base_class, default_value, help=None):
      """create a configurable CaselessStrEnum traitlet from baseclass
  
      the enumeration should contain all names of non_abstract_children()
      of said baseclass and the default choice should be given by
      `base_class._default` name.
  
      default must be specified and must be the name of one child-class
      """
      if help is None:
          help = "{} to use.".format(base_class.__name__)
  
      choices = [cls.__name__ for cls in non_abstract_children(base_class)]
  
      if default_value not in choices:
          raise ValueError(f"{default_value} is not in choices: {choices}")
  
      return CaselessStrEnum(
          choices, default_value=default_value, allow_none=False, help=help,
      ).tag(config=True)
  
  
  def classes_with_traits(base_class):
      """ Returns a list of the base class plus its non-abstract children
      if they have traits """
      all_classes = [base_class] + non_abstract_children(base_class)
      return [cls for cls in all_classes if has_traits(cls)]
  
  
  def has_traits(cls, ignore=("config", "parent")):
      """True if cls has any traits apart from the usual ones
  
      all our components have at least 'config' and 'parent' as traitlets
      this is inherited from `traitlets.config.Configurable` so we ignore them
      here.
      """
      return bool(set(cls.class_trait_names()) - set(ignore))
  
  
  class TelescopePatternList(UserList):
      """
      Representation for a list of telescope pattern tuples. This is a helper class
      used  by the Trait TelescopeParameter as its value type
      """
  
      def __init__(self, *args):
          super().__init__(*args)
          self._lookup = None
          self._subarray = None
  
      @property
      def tel(self):
          """ access the value per telescope_id, e.g. `param.tel[2]`"""
          if self._lookup:
              return self._lookup
          else:
              raise RuntimeError(
                  "No TelescopeParameterLookup was registered. You must "
                  "call attach_subarray() first"
              )
  
      def attach_subarray(self, subarray: "ctapipe.instrument.SubarrayDescription"):
          """
          Register a SubarrayDescription so that the user-specified values can be
          looked up by tel_id. This must be done before using the `.tel[x]` property
          """
          self._subarray = subarray
          self._lookup.attach_subarray(subarray)
  
  
  class TelescopeParameterLookup:
      def __init__(self, telescope_parameter_list):
          """
          Handles the lookup of corresponding configuration value from a list of
          tuples for a telid.
  
          Parameters
          ----------
          telescope_parameter_list : list
              List of tuples in the form `[(command, argument, value), ...]`
          """
          # self._telescope_parameter_list = copy.deepcopy(telescope_parameter_list)
          self._telescope_parameter_list = copy.deepcopy(telescope_parameter_list)
          self._value_for_tel_id = None
          self._subarray = None
          self._subarray_global_value = None
          for param in telescope_parameter_list:
              if param[1] == "*":
                  self._subarray_global_value = param[2]
  
      def attach_subarray(self, subarray):
          """
          Prepare the TelescopeParameter by informing it of the
          subarray description
  
          Parameters
          ----------
          subarray: ctapipe.instrument.SubarrayDescription
              Description of the subarray
              (includes mapping of tel_id to tel_type)
          """
          self._subarray = subarray
          self._value_for_tel_id = {}
          for command, arg, value in self._telescope_parameter_list:
              if command == "type":
                  matched_tel_types = [
                      str(t) for t in subarray.telescope_types if fnmatch(str(t), arg)
                  ]
                  logger.debug(f"argument '{arg}' matched: {matched_tel_types}")
                  if len(matched_tel_types) == 0:
                      logger.warning(
                          "TelescopeParameter type argument '%s' did not match "
                          "any known telescope types",
                          arg,
                      )
                  for tel_type in matched_tel_types:
                      for tel_id in subarray.get_tel_ids_for_type(tel_type):
                          self._value_for_tel_id[tel_id] = value
              elif command == "id":
                  self._value_for_tel_id[int(arg)] = value
              else:
                  raise ValueError(f"Unrecognized command: {command}")
  
      def __getitem__(self, tel_id: Optional[int]):
          """
          Returns the resolved parameter for the given telescope id
          """
          if tel_id is None:
              if self._subarray_global_value is not None:
                  return self._subarray_global_value
              else:
                  raise KeyError("No subarray global value set for TelescopeParameter")
          if self._value_for_tel_id is None:
              raise ValueError(
                  "TelescopeParameterLookup: No subarray attached, call "
                  "`attach_subarray` first before trying to access a value by tel_id"
              )
          try:
              return self._value_for_tel_id[tel_id]
          except KeyError:
              raise KeyError(
                  f"TelescopeParameterLookup: no "
                  f"parameter value was set for telescope with tel_id="
                  f"{tel_id}. Please set it explicitly, "
                  f"or by telescope type or '*'."
              )
  
  
  class TelescopeParameter(List):
      """
      Allow a parameter value to be specified as a simple value (of type *dtype*),
      or as a list of patterns that match different telescopes.
      The patterns are given as a list of 3-tuples in in the
      form: `[(command, argument, value), ...]`.
  
      Command can be one of:
      - 'type': argument is then a telescope type  string (e.g.
         `('type', 'SST_ASTRI_CHEC', 4.0)` to apply to all telescopes of that type,
         or use a wildcard like "LST*", or "*" to set a pure default value for all
         telescopes.
      - 'id':  argument is a specific telescope ID `['id', 89, 5.0]`)
  
      These are evaluated in-order, so you can first set a default value, and then set
      values for specific telescopes or types to override them.
  
      Examples
      --------
  
      .. code-block: python
      tel_param = [
          ('type', '*', 5.0),                       # default for all
          ('type', 'LST_*', 5.2),
          ('type', 'MST_MST_NectarCam', 4.0),
          ('type', 'MST_MST_FlashCam', 4.5),
          ('id', 34, 4.0),                   # override telescope 34 specifically
      ]
  
      .. code-block: python
      tel_param = 4.0  # sets this value for all telescopes
  
      """
  
      klass = TelescopePatternList
  
      def __init__(self, dtype=float, default_value=None, **kwargs):
          if not isinstance(dtype, type):
              raise ValueError("dtype should be a type")
          if isinstance(default_value, dtype):
              default_value = [("type", "*", default_value)]
          super().__init__(default_value=default_value, **kwargs)
          self._dtype = dtype
  
      def validate(self, obj, value):
          # Support a single value for all (convert into a default value)
          if isinstance(value, self._dtype):
              value = [("type", "*", value)]
  
          # Check each value of list
          normalized_value = TelescopePatternList(None)
          if isinstance(value, self._dtype):
              value = [("type", "*", value)]
          if isinstance(value, (UserList, list)):
              for pattern in value:
                  # now check for the standard 3-tuple of (command, argument, value)
                  if len(pattern) != 3:
                      raise TraitError(
                          "pattern should be a tuple of (command, argument, value)"
                      )
                  command, arg, val = pattern
                  if not isinstance(val, self._dtype):
                      raise TraitError(f"Value should be a {self._dtype}")
                  if not isinstance(command, str):
                      raise TraitError("command must be a string")
                  if command not in ["type", "id"]:
                      raise TraitError("command must be one of: '*', 'type', 'id'")
                  if command == "type":
                      if not isinstance(arg, str):
                          raise TraitError("'type' argument should be a string")
                  if command == "id":
                      try:
                          arg = int(arg)
                      except ValueError:
                          raise TraitError(
                              f"Argument of 'id' should be an int (got '{arg}')"
                          )
  
                  val = self._dtype(val)
                  normalized_value.append((command, arg, val))
                  normalized_value._lookup = TelescopeParameterLookup(normalized_value)
  
                  if (
                      isinstance(value, TelescopePatternList)
                      and value._subarray is not None
                  ):
                      normalized_value.attach_subarray(value._subarray)
  
          else:
              raise TraitError(f"Value should be a {self._dtype}")
  
          return normalized_value
  
      def set(self, obj, value):
          # Retain existing subarray description
          # when setting new value for TelescopeParameter
          try:
              old_value = obj._trait_values[self.name]
          except KeyError:
              old_value = self.default_value
          super().set(obj, value)
          if getattr(old_value, "_subarray", None) is not None:
              obj._trait_values[self.name].attach_subarray(old_value._subarray)
  
  
  class FloatTelescopeParameter(TelescopeParameter):
      """ a `TelescopeParameter` with float type (see docs for `TelescopeParameter`)"""
  
      def __init__(self, **kwargs):
          super().__init__(dtype=float, **kwargs)
  
  
  class IntTelescopeParameter(TelescopeParameter):
      """ a `TelescopeParameter` with int type (see docs for `TelescopeParameter`)"""
  
      def __init__(self, **kwargs):
          super().__init__(dtype=int, **kwargs)
  
  
  class BoolTelescopeParameter(TelescopeParameter):
      """ a `TelescopeParameter` with int type (see docs for `TelescopeParameter`)"""
  
      def __init__(self, **kwargs):
          super().__init__(dtype=bool, **kwargs)
  