text: import typing as t
  
  from .exception import LtiException
  from .lineitem import LineItem
  
  if t.TYPE_CHECKING:
      from .service_connector import ServiceConnector, _ServiceConnectorResponse
      from .grade import Grade
      from mypy_extensions import TypedDict
      from typing_extensions import Literal
  
      _AssignmentsGradersData = TypedDict('_AssignmentsGradersData', {
          'scope': t.List[Literal['https://purl.imsglobal.org/spec/lti-ags/scope/score',
                                  'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem',
                                  'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly']],
          'lineitems': str,
          'lineitem': str,
      }, total=False)
  
  
  class AssignmentsGradesService(object):
      _service_connector = None  # type: ServiceConnector
      _service_data = None  # type: _AssignmentsGradersData
  
      def __init__(self, service_connector, service_data):
          # type: (ServiceConnector, _AssignmentsGradersData) -> None
          self._service_connector = service_connector
          self._service_data = service_data
  
      def put_grade(self, grade, lineitem=None):
          # type: (Grade, t.Optional[LineItem]) -> _ServiceConnectorResponse
          """
          Send grade to the LTI platform.
  
          :param grade: Grade instance
          :param lineitem: LineItem instance
          :return: dict with HTTP response body and headers
          """
  
          if "https://purl.imsglobal.org/spec/lti-ags/scope/score" not in self._service_data['scope']:
              raise LtiException('Missing required scope')
  
          if lineitem and not lineitem.get_id():
              lineitem = self.find_or_create_lineitem(lineitem)
              score_url = lineitem.get_id()
          elif not lineitem and self._service_data.get('lineitem'):
              score_url = self._service_data.get('lineitem')
          else:
              if not lineitem:
                  lineitem = LineItem()
                  lineitem.set_label('default')\
                      .set_score_maximum(100)
                  lineitem = self.find_or_create_lineitem(lineitem)
              score_url = lineitem.get_id()
  
          assert score_url is not None
          score_url = self._add_url_path_ending(score_url, 'scores')
          return self._service_connector.make_service_request(
              self._service_data['scope'],
              score_url,
              is_post=True,
              data=grade.get_value(),
              content_type='application/vnd.ims.lis.v1.score+json'
          )
  
      def get_lineitems_page(self, lineitems_url=None):
          # type: (t.Optional[str]) -> t.Tuple[list, t.Optional[str]]
          """
          Get one page with line items.
  
          :param lineitems_url: LTI platform's URL (optional)
          :return: tuple in format: (list with line items, next page url)
          """
          if "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem" not in self._service_data['scope']:
              raise LtiException('Missing required scope')
  
          if not lineitems_url:
              lineitems_url = self._service_data['lineitems']
  
          lineitems = self._service_connector.make_service_request(
              self._service_data['scope'],
              lineitems_url,
              accept='application/vnd.ims.lis.v2.lineitemcontainer+json'
          )
          if not isinstance(lineitems['body'], list):
              raise LtiException('Unknown response type received for line items')
          return lineitems['body'], lineitems['next_page_url']
  
      def get_lineitems(self):
          # type: () -> list
          """
          Get list of all available line items.
  
          :return: list
          """
          lineitems_res_lst = []
          lineitems_url = self._service_data['lineitems']  # type: t.Optional[str]
  
          while lineitems_url:
              lineitems, lineitems_url = self.get_lineitems_page(lineitems_url)
              lineitems_res_lst.extend(lineitems)
  
          return lineitems_res_lst
  
      def find_lineitem(self, prop_name, prop_value):
          # type: (str, t.Any) -> t.Optional[LineItem]
          """
          Find line item by some property (ID/Tag).
  
          :param prop_name: property name
          :param prop_value: property value
          :return: LineItem instance or None
          """
          lineitems_url = self._service_data['lineitems']  # type: t.Optional[str]
  
          while lineitems_url:
              lineitems, lineitems_url = self.get_lineitems_page(lineitems_url)
              for lineitem in lineitems:
                  lineitem_prop_value = lineitem.get(prop_name)
                  if lineitem_prop_value == prop_value:
                      return LineItem(lineitem)
          return None
  
      def find_lineitem_by_id(self, ln_id):
          # type: (str) -> t.Optional[LineItem]
          """
          Find line item by ID.
  
          :param ln_id: str
          :return: LineItem instance or None
          """
          return self.find_lineitem('id', ln_id)
  
      def find_lineitem_by_tag(self, tag):
          # type: (str) -> t.Optional[LineItem]
          """
          Find line item by Tag.
  
          :param tag: str
          :return: LineItem instance or None
          """
          return self.find_lineitem('tag', tag)
  
      def find_or_create_lineitem(self, new_lineitem, find_by='tag'):
          # type: (LineItem, Literal['tag', 'id']) -> LineItem
          """
          Try to find line item using ID or Tag. New lime item will be created if nothing is found.
  
          :param new_lineitem: LineItem instance
          :param find_by: str ("tag"/"id")
          :return: LineItem instance (based on response from the LTI platform)
          """
          if find_by == 'tag':
              tag = new_lineitem.get_tag()
              if not tag:
                  raise LtiException('Tag value is not specified')
              lineitem = self.find_lineitem_by_tag(tag)
          elif find_by == 'id':
              line_id = new_lineitem.get_id()
              if not line_id:
                  raise LtiException('ID value is not specified')
              lineitem = self.find_lineitem_by_id(line_id)
          else:
              raise LtiException('Invalid "find_by" value: ' + str(find_by))
  
          if lineitem:
              return lineitem
  
          created_lineitem = self._service_connector.make_service_request(
              self._service_data['scope'],
              self._service_data['lineitems'],
              is_post=True,
              data=new_lineitem.get_value(),
              content_type='application/vnd.ims.lis.v2.lineitem+json',
              accept='application/vnd.ims.lis.v2.lineitem+json'
          )
          if not isinstance(created_lineitem['body'], dict):
              raise LtiException('Unknown response type received for create line item')
          return LineItem(created_lineitem['body'])
  
      def get_grades(self, lineitem):
          # type: (LineItem) -> list
          """
          Return all grades for the passed line item (across all users enrolled in the line item's context).
  
          :param lineitem: LineItem instance
          :return: list of grades
          """
          lineitem_id = lineitem.get_id()
          lineitem_tag = lineitem.get_tag()
  
          find_by = None  # type: t.Optional[Literal['id', 'tag']]
          if lineitem_id:
              find_by = 'id'
          elif lineitem_tag:
              find_by = 'tag'
          else:
              raise LtiException('Received LineItem did not contain a tag or id')
  
          lineitem = self.find_or_create_lineitem(lineitem, find_by=find_by)
          lineitem_id = lineitem.get_id()
          assert lineitem_id is not None
          results_url = self._add_url_path_ending(lineitem_id, 'results')
          scores = self._service_connector.make_service_request(
              self._service_data['scope'],
              results_url,
              accept='application/vnd.ims.lis.v2.resultcontainer+json'
          )
          if not isinstance(scores['body'], list):
              raise LtiException('Unknown response type received for results')
          return scores['body']
  
      def _add_url_path_ending(self, url, url_path_ending):
          # type: (str, str) -> str
          if '?' in url:
              url_parts = url.split('?')
              new_url = url_parts[0]
              new_url += '' if new_url.endswith('/') else '/'
              return new_url + url_path_ending + '?' + url_parts[1]
          else:
              url += '' if url.endswith('/') else '/'
              return url + url_path_ending
  