text: from lxml import objectify
  
  from kloppy.domain import (
      Period,
      PitchDimensions,
      Dimension,
      Team,
      Score,
      Ground,
      DatasetFlag,
      AttackingDirection,
      Orientation,
      Position,
      Point,
      Provider,
  )
  from kloppy.infra.utils import Readable
  
  from .models import *
  
  
  def noop(x):
      return x
  
  
  def _load_provider_parameters(parent_elm, value_mapper=None) -> Dict:
      if parent_elm is None:
          return {}
  
      if not value_mapper:
          value_mapper = noop
  
      return {
          str(param.find("Name")): value_mapper(param.find("Value"))
          for param in parent_elm.iterchildren(tag="ProviderParameter")
          if param.find("Value") != ""
      }
  
  
  def _load_periods(global_config_elm, frame_rate: int) -> List[Period]:
      provider_params = _load_provider_parameters(
          global_config_elm.find("ProviderGlobalParameters"), value_mapper=int
      )
  
      period_names = [
          "first_half",
          "second_half",
          "first_extra_half",
          "second_extra_half",
      ]
  
      periods = []
  
      for idx, period_name in enumerate(period_names):
          start_key = f"{period_name}_start"
          end_key = f"{period_name}_end"
          if start_key in provider_params:
              periods.append(
                  Period(
                      id=idx + 1,
                      start_timestamp=float(provider_params[start_key])
                      / frame_rate,
                      end_timestamp=float(provider_params[end_key]) / frame_rate,
                  )
              )
          else:
              # done
              break
  
      return periods
  
  
  def _load_players(players_elm, team: Team) -> List[Player]:
      return [
          Player(
              team=team,
              jersey_no=int(player_elm.find("ShirtNumber")),
              player_id=player_elm.attrib["id"],
              name=str(player_elm.find("Name")),
              position=_load_position_data(
                  player_elm.find("ProviderPlayerParameters")
              ),
              attributes=_load_provider_parameters(
                  player_elm.find("ProviderPlayerParameters")
              ),
          )
          for player_elm in players_elm.iterchildren(tag="Player")
          if player_elm.attrib["teamId"] == team.team_id
      ]
  
  
  def _load_position_data(parent_elm) -> Position:
      # TODO: _load_provider_parameters is called twice to set position data
      # and then again to set the attributes. Also, data in position should not
      # be duplicated in attributes either.
      player_provider_parameters = _load_provider_parameters(parent_elm)
      if "position_index" not in player_provider_parameters:
          return None
  
      return Position(
          position_id=player_provider_parameters["position_index"],
          name=player_provider_parameters["position_type"],
          coordinates=Point(
              player_provider_parameters["position_x"],
              player_provider_parameters["position_y"],
          ),
      )
  
  
  def _load_data_format_specifications(
      data_format_specifications_elm,
  ) -> List[DataFormatSpecification]:
      return [
          DataFormatSpecification.from_xml_element(data_format_specification_elm)
          for data_format_specification_elm in data_format_specifications_elm.iterchildren(
              tag="DataFormatSpecification"
          )
      ]
  
  
  def _load_sensors(sensors_elm) -> List[Sensor]:
      return [
          Sensor.from_xml_element(sensor_elm)
          for sensor_elm in sensors_elm.iterchildren(tag="Sensor")
      ]
  
  
  def _load_pitch_dimensions(
      metadata_elm, sensors: List[Sensor]
  ) -> Union[None, PitchDimensions]:
  
      normalized = False
      for sensor in sensors:
          if sensor.sensor_id == "position":
              if sensor.channels[0].unit == "normalized":
                  normalized = True
                  break
  
      field_size_path = objectify.ObjectPath("Metadata.Sessions.Session[0]")
      field_size_elm = field_size_path.find(metadata_elm).find("FieldSize")
  
      if field_size_elm is not None and normalized:
          return PitchDimensions(
              x_dim=Dimension(0, 1),
              y_dim=Dimension(0, 1),
              x_per_meter=1 / int(field_size_elm.find("Width")),
              y_per_meter=1 / int(field_size_elm.find("Height")),
          )
      else:
          return None
  
  
  def load_metadata(
      metadata_file: Readable, provider: Provider = None
  ) -> EPTSMetadata:
      root = objectify.fromstring(metadata_file.read())
      metadata = root.find("Metadata")
  
      score_path = objectify.ObjectPath(
          "Metadata.Sessions.Session[0].MatchParameters.Score"
      )
      score_elm = score_path.find(metadata)
      score = Score(
          home=score_elm.LocalTeamScore, away=score_elm.VisitingTeamScore
      )
  
      _team_map = {
          Ground.HOME: score_elm.attrib["idLocalTeam"],
          Ground.AWAY: score_elm.attrib["idVisitingTeam"],
      }
  
      _team_name_map = {
          team_elm.attrib["id"]: str(team_elm.find("Name"))
          for team_elm in metadata.find("Teams").iterchildren(tag="Team")
      }
  
      teams_metadata = {}
      for ground, team_id in _team_map.items():
          team = Team(
              team_id=team_id, name=_team_name_map[team_id], ground=ground
          )
          team.players = _load_players(metadata.find("Players"), team)
          teams_metadata.update({ground: team})
  
      data_format_specifications = _load_data_format_specifications(
          root.find("DataFormatSpecifications")
      )
  
      device_path = objectify.ObjectPath("Metadata.Devices.Device[0].Sensors")
      sensors = _load_sensors(device_path.find(metadata))
  
      _channel_map = {
          channel.channel_id: channel
          for sensor in sensors
          for channel in sensor.channels
      }
  
      _all_players = [
          player
          for key, value in teams_metadata.items()
          for player in value.players
      ]
  
      _player_map = {player.player_id: player for player in _all_players}
  
      player_channels = [
          PlayerChannel(
              player_channel_id=player_channel_elm.attrib["id"],
              player=_player_map[player_channel_elm.attrib["playerId"]],
              channel=_channel_map[player_channel_elm.attrib["channelId"]],
          )
          for player_channel_elm in metadata.find("PlayerChannels").iterchildren(
              tag="PlayerChannel"
          )
      ]
  
      frame_rate = int(metadata.find("GlobalConfig").find("FrameRate"))
      pitch_dimensions = _load_pitch_dimensions(metadata, sensors)
      periods = _load_periods(metadata.find("GlobalConfig"), frame_rate)
  
      if periods:
          start_attacking_direction = periods[0].attacking_direction
      else:
          start_attacking_direction = None
  
      orientation = (
          (
              Orientation.FIXED_HOME_AWAY
              if start_attacking_direction == AttackingDirection.HOME_AWAY
              else Orientation.FIXED_AWAY_HOME
          )
          if start_attacking_direction != AttackingDirection.NOT_SET
          else None
      )
  
      metadata.orientation = orientation
  
      return EPTSMetadata(
          teams=list(teams_metadata.values()),
          periods=periods,
          pitch_dimensions=pitch_dimensions,
          data_format_specifications=data_format_specifications,
          player_channels=player_channels,
          frame_rate=frame_rate,
          sensors=sensors,
          score=score,
          orientation=None,
          provider=provider,
          flags=~(DatasetFlag.BALL_STATE | DatasetFlag.BALL_OWNING_TEAM),
      )
  