content: # Copyright Contributors to the Amundsen project.
  # SPDX-License-Identifier: Apache-2.0
  
  import collections
  import json
  import logging
  from abc import abstractmethod
  from datetime import date, datetime, timedelta
  from operator import attrgetter
  from typing import (Any, Callable, Dict, Iterable, List, Mapping, Optional,
                      Sequence, Set, Tuple, Type, TypeVar, Union, no_type_check,
                      overload)
  from urllib.parse import unquote
  
  import gremlin_python
  from amundsen_common.entity.resource_type import ResourceType
  from amundsen_common.models.dashboard import DashboardSummary
  from amundsen_common.models.feature import Feature
  from amundsen_common.models.generation_code import GenerationCode
  from amundsen_common.models.lineage import Lineage, LineageItem
  from amundsen_common.models.popular_table import PopularTable
  from amundsen_common.models.table import (Application, Badge, Column,
                                            ProgrammaticDescription, Reader,
                                            Source, Stat, Table, Tag, Watermark)
  from amundsen_common.models.user import User
  from amundsen_gremlin.gremlin_model import (EdgeType, EdgeTypes, VertexType,
                                              VertexTypes, WellKnownProperties)
  from amundsen_gremlin.gremlin_shared import \
      append_traversal as _append_traversal  # TODO: rename the references
  from amundsen_gremlin.gremlin_shared import (make_column_uri,
                                               make_description_uri)
  from amundsen_gremlin.neptune_bulk_loader.gremlin_model_converter import \
      ensure_vertex_type
  from amundsen_gremlin.script_translator import (
      ScriptTranslator, ScriptTranslatorTargetJanusgraph)
  from amundsen_gremlin.test_and_development_shard import get_shard
  from flask import current_app, has_app_context
  from gremlin_python.driver.client import Client
  from gremlin_python.driver.driver_remote_connection import \
      DriverRemoteConnection
  from gremlin_python.driver.resultset import ResultSet
  from gremlin_python.driver.tornado.transport import TornadoTransport
  from gremlin_python.process.anonymous_traversal import traversal
  from gremlin_python.process.graph_traversal import (GraphTraversal,
                                                      GraphTraversalSource, V,
                                                      __, bothV, coalesce,
                                                      constant, has, inE, inV,
                                                      outE, outV, select, unfold,
                                                      valueMap, values)
  from gremlin_python.process.traversal import Cardinality
  from gremlin_python.process.traversal import Column as MapColumn
  from gremlin_python.process.traversal import (Direction, Order, P, T, TextP,
                                                Traversal, gte, not_, within,
                                                without)
  from gremlin_python.structure.graph import Path
  from neptune_python_utils.gremlin_utils import ExtendedGraphSONSerializersV3d0
  from overrides import overrides
  from tornado import httpclient
  from typing_extensions import Protocol  # TODO: it's in typing 3.8
  
  from metadata_service import config
  from metadata_service.entity.dashboard_detail import \
      DashboardDetail as DashboardDetailEntity
  from metadata_service.entity.dashboard_query import \
      DashboardQuery as DashboardQueryEntity
  from metadata_service.entity.description import Description
  from metadata_service.entity.tag_detail import TagDetail
  from metadata_service.exception import NotFoundException
  from metadata_service.proxy.statsd_utilities import timer_with_counter
  from metadata_service.util import UserResourceRel
  
  from .base_proxy import BaseProxy
  from .shared import checkNotNone, retrying
  
  # don't use statics.load_statics(globals()) it plays badly with mypy
  
  __all__ = ['AbstractGremlinProxy', 'GenericGremlinProxy']
  
  LOGGER = logging.getLogger(__name__)
  PUBLISH_TAG_TIME_FORMAT: str = "%Y-%m-%d %H:%M"
  AMUNDSEN_TIMESTAMP_KEY: str = 'amundsen_updated_timestamp'
  
  
  def timestamp() -> datetime:
      """
      mostly for mocking
      See also https://docs.aws.amazon.com/neptune/latest/userguide/best-practices-gremlin-datetime-glv.html
      and DateIO in gremlin python
      """
      return datetime.now()
  
  
  def is_reasonable_vertex_label(label: str) -> bool:
      vertex_labels = set([each.value.label for each in VertexTypes])
      return label in vertex_labels
  
  
  def get_label_from(_entity_type_or_enum_or_str: Union[str, VertexTypes, EdgeTypes, VertexType, EdgeType]) -> str:
      if isinstance(_entity_type_or_enum_or_str, str):
          return _entity_type_or_enum_or_str
      elif isinstance(_entity_type_or_enum_or_str, (VertexTypes, EdgeTypes)):
          return _entity_type_or_enum_or_str.value.label
      elif isinstance(_entity_type_or_enum_or_str, (VertexType, EdgeType)):
          return _entity_type_or_enum_or_str.label
      else:
          raise RuntimeError(f'what the heck is label? {type(_entity_type_or_enum_or_str)} {_entity_type_or_enum_or_str}')
  
  
  def get_cardinality_for(_entity_type_or_enum: Union[VertexTypes, EdgeTypes, VertexType, EdgeType],
                          name: str) -> Optional[Cardinality]:
      _entity_type: Union[VertexType, EdgeType]
      if isinstance(_entity_type_or_enum, (VertexTypes, EdgeTypes)):
          _entity_type = _entity_type_or_enum.value
      elif isinstance(_entity_type_or_enum, (VertexType, EdgeType)):
          _entity_type = _entity_type_or_enum
      else:
          raise AssertionError(f'thing is not a VertexType(s) or EdgeType(s): {_entity_type_or_enum}')
      properties = _entity_type.properties_as_map()
  
      # TODO: this will expose missing properties
      if name not in properties:
          raise AssertionError(f'missing {name} property in {_entity_type_or_enum} {properties}')
  
      maybe = properties[name].cardinality
      if isinstance(_entity_type, VertexType):
          return maybe.value if maybe is not None else Cardinality.single
      elif isinstance(_entity_type, EdgeType):
          return maybe.value if maybe is not None else None
      else:
          raise AssertionError(f'thing is not a VertexType(s) or EdgeType(s): {_entity_type_or_enum}')
  
  
  class FromResultSet:
      @classmethod
      def generator(cls, result_set: ResultSet) -> Iterable[Any]:
          for part in result_set:
              for item in part:
                  yield item
  
      @classmethod
      def iterate(cls, result_set: ResultSet) -> None:
          # haiku for consuming an interator
          collections.deque(cls.generator(result_set), maxlen=0)
  
      @classmethod
      def next(cls, result_set: ResultSet) -> Any:
          """
          really this is like first, but
          """
          return next(iter(cls.generator(result_set)))
  
      @classmethod
      def toList(cls, result_set: ResultSet) -> List:
          return list(cls.generator(result_set))
  
      @classmethod
      def toSet(cls, result_set: ResultSet) -> Set:
          return set(cls.generator(result_set))
  
      @classmethod
      def getOptional(cls, result_set: ResultSet) -> Optional[Any]:
          try:
              return cls.getOnly(result_set)
          except StopIteration:
              return None
  
      @classmethod
      def getOnly(cls, result_set: ResultSet) -> Any:
          i = iter(cls.generator(result_set))
          value = next(i)
          try:
              next(i)
          except StopIteration:
              return value
          raise RuntimeError('Expected one item, but there was more!')
  
  
  TYPE = TypeVar('TYPE')
  
  
  class ExecuteQuery(Protocol):
      @overload  # noqa: F811
      def __call__(self, query: Traversal, get: Callable[[ResultSet], V]) -> V:
          ...
  
      @overload  # noqa: F811
      def __call__(self, query: str, get: Callable[[ResultSet], V], *,  # noqa: F811
                   bindings: Optional[Mapping[str, Any]] = None) -> V:
          ...
  
      def __call__(self, query: Union[str, Traversal], get: Callable[[ResultSet], V], *,  # noqa: F811
                   bindings: Optional[Mapping[str, Any]] = None) -> V:
          ...
  
  
  class ClientQueryExecutor(ExecuteQuery):
      def __init__(self, *, client: Client, traversal_translator: Callable[[Traversal], str]) -> None:
          self.client = client
          self.traversal_translator = traversal_translator
  
      def __call__(self, query: Union[str, Traversal], get: Callable[[ResultSet], V], *,  # noqa: F811
                   bindings: Optional[Mapping[str, Any]] = None) -> V:
          if isinstance(query, Traversal):
              if bindings is not None:
                  raise AssertionError(f'expected bindings to be none')
              query_text = self.traversal_translator(query)
          else:
              query_text = query
  
          if not isinstance(query_text, str):
              raise AssertionError(f'expected str')
          result_set = self.client.submit(query_text, bindings)
          return get(result_set)
  
  
  class RetryingClientQueryExecutor(ClientQueryExecutor):
      def __init__(self, client: Client, traversal_translator: Callable[[Traversal], str],
                   is_retryable: Callable[[Exception], bool]) -> None:
          ClientQueryExecutor.__init__(self, client=client, traversal_translator=traversal_translator)
          self.is_retryable = is_retryable
  
      def __enter__(self) -> Any:
          return self
  
      def __exit__(self, *args: Any, **kwargs: Any) -> None:
          return self.client.close()
  
      # TODO: ideally this would be __call__(*args: Any, **kwargs: Any) -> Any (and then this could be mixinable) but I
      # can't get mypy to not think that conflicts
      def __call__(self, query: Union[str, Traversal], get: Callable[[ResultSet], V], *,
                   bindings: Optional[Mapping[str, Any]] = None) -> V:
          def callable() -> V:
              return ClientQueryExecutor.__call__(self, query, get, bindings=bindings)
          try:
              return retrying(callable, is_retryable=self.is_retryable)
          except Exception as e:
              LOGGER.warning(f'got exception executing query={query}, get={get}, bindings={bindings}', exc_info=e)
              raise
  
  
  @no_type_check
  def _safe_get_any(root, *keys):
      """
      Helper method for getting value from nested dict, special for Gremlin valueMap things where properties can be lists
      of one or 0.
      :param root:
      :param keys:
      :return:
      """
      current = root
      for key in keys:
          # first get the only element if it's a list/set
          if isinstance(current, Sequence):
              if len(current) > 1:
                  raise RuntimeError(f'{current} is not a singleton!  root={root} keys={keys}')
              elif len(current) == 0:
                  current = None
              else:
                  current = current[0]
          if not current:
              return None
          if not isinstance(current, Mapping):
              raise RuntimeError(f'{current} is not a Mapping!  root={root} keys={keys}')
          current = current.get(key)
          if not current:
              return None
      # don't dereference the list this usually is, we might want it or not
      return current
  
  
  @no_type_check
  def _safe_get_list(root, *keys, transform: Optional[Callable] = None):
      """
      Like _safe_get_any, but for gremlin where we get a list for a single property
      """
      values = _safe_get_any(root, *keys)
      # is List the only type? it seems so
      if values is None:
          return None
      elif not isinstance(values, List):
          raise RuntimeError(f'{values} is not a List!  root={root} keys={keys}')
      elif transform is None:
          return sorted(values)
      elif len(values) > 0 and type(values[0]) is datetime and transform == int:
          # need to do something special for datetimes we are transforming into int's
          return sorted([transform(value.timestamp()) for value in values])
      else:
          return sorted([transform(value) for value in values])
  
  
  @no_type_check
  def _safe_get(root, *keys, transform: Optional[Callable] = None, default: Any = None):
      """
      Like _safe_get_any, but for gremlin where we get a list for a single property
      """
      value = _safe_get_list(root, *keys, transform=transform)
      if value is None or len(value) == 0:
          return default
      elif len(value) > 1:
          raise RuntimeError(f'{value} is not a singleton!  root={root} keys={keys}')
      else:
          return value[0]
  
  
  def _properties_or_drop_if_changed_except(
          *excludes: str, label: Union[VertexTypes, EdgeTypes, VertexType, EdgeType],
          thing: Union[object, Dict[str, Any]], existing: Union[object, Dict[str, Any]]) -> GraphTraversal:
      if isinstance(thing, Mapping):
          _thing = thing
      elif hasattr(thing, '__dict__'):
          _thing = vars(thing)
      else:
          raise AssertionError(f'thing must be a dict or have __dict__: {type(thing)}')
  
      if isinstance(existing, Mapping):
          _existing = existing
      elif hasattr(existing, '__dict__'):
          _existing = vars(existing)
      else:
          raise AssertionError(f'existing must be a dict or have __dict__: {type(existing)}')
  
      def p(name: str) -> Optional[GraphTraversal]:
          return _property_or_drop_if_changed(name=name, value=_thing.get(name, None), existing=_existing.get(name, None),
                                              cardinality=get_cardinality_for(label, name))
  
      names = sorted(set(_thing.keys()).difference(set(excludes)))
      traversals = [t for t in [p(name) for name in names] if t is not None]
      return _append_traversal(__.start(), *traversals) if traversals else None
  
  
  def _property_unchanged(*, value: Any, existing: Any, cardinality: Optional[Cardinality]) -> bool:
      # this is the usual case: either an Edge property (no cardinality) or Vertex property with Cardinality.single
      if existing is None:
          return value is None
      elif cardinality is None or cardinality == Cardinality.single:
          return tuple(existing) == (value,)
      elif cardinality in (Cardinality.set_, Cardinality.list_):
          return value in tuple(existing)
      else:
          return False
  
  
  def _property_or_drop_if_changed(*, name: str, value: Any, existing: Any, cardinality: Optional[Cardinality]) \
          -> Optional[GraphTraversal]:
      """
      You want to use _vertex_property or _edge_property.
      """
      if _property_unchanged(value=value, existing=existing, cardinality=cardinality):
          return None
      elif value is None:
          return __.sideEffect(__.properties(name).drop())
      else:
          # complicated: edges can't have cardinality supplied and are implied to be single
          if cardinality is None:
              return __.property(name, value)
          else:
              return __.property(cardinality, name, value)
  
  
  def _properties_or_drop_except(label: Union[VertexTypes, EdgeTypes, VertexType, EdgeType],
                                 thing: Union[object, Dict[str, Any]], *excludes: str) -> GraphTraversal:
      if isinstance(thing, Mapping):
          pass
      elif hasattr(thing, '__dict__'):
          thing = vars(thing)
      else:
          raise AssertionError(f'must be a dict or have __dict__: {type(thing)}')
      g = __.start()
      for name in set(thing.keys()).difference(set(excludes)):
          g = _property_or_drop(g=g, name=name, value=thing.get(name, None), cardinality=get_cardinality_for(label, name))
      return g
  
  
  def _properties_or_drop_of(label: Union[VertexTypes, EdgeTypes, VertexType, EdgeType],
                             thing: Union[object, Dict[str, Any]], *includes: str) -> GraphTraversal:
      if isinstance(thing, Mapping):
          pass
      elif hasattr(thing, '__dict__'):
          thing = vars(thing)
      else:
          raise AssertionError(f'must be a dict or have __dict__: {type(thing)}')
      g = __.start()
      for name in includes:
          g = _property_or_drop(g=g, name=name, value=thing.get(name, None), cardinality=get_cardinality_for(label, name))
      return g
  
  
  def _properties_except(thing: Union[object, Dict[str, Any]], *excludes: str) -> Mapping[str, Any]:
      if isinstance(thing, Mapping):
          pass
      elif hasattr(thing, '__dict__'):
          thing = vars(thing)
      else:
          raise AssertionError(f'must be a dict or have __dict__: {type(thing)}')
      _excludes = set(excludes)
      return dict([(k, v) for k, v in thing.items() if k not in _excludes])
  
  
  def _properties_of(thing: Union[object, Dict[str, Any]], *includes: str) -> Mapping[str, Any]:
      if isinstance(thing, Mapping):
          pass
      elif hasattr(thing, '__dict__'):
          thing = vars(thing)
      else:
          raise AssertionError(f'must be a dict or have __dict__: {type(thing)}')
      return dict([(k, thing.get(k, None)) for k in includes])
  
  
  def _is_select_traversal(g: GraphTraversal) -> Optional[str]:
      if not g.bytecode.source_instructions and len(g.bytecode.step_instructions) == 1 and \
         len(g.bytecode.step_instructions[0]) == 2 and g.bytecode.step_instructions[0][0] == 'select':
          return g.bytecode.step_instructions[0][1]
      else:
          return None
  
  
  def _vertex_property(*, g: GraphTraversal, name: str, value: Any, cardinality: Optional[Cardinality] = None,
                       label: Optional[VertexTypes] = None) -> GraphTraversal:
  
      if (cardinality is None) and (label is None):
          raise AssertionError('must pass one of label or cardinality')
      if cardinality is not None:
          _cardinality = cardinality
      elif label is not None:
          _cardinality = get_cardinality_for(label, name)
      else:
          raise RuntimeError()
  
      return _property_or_drop(g=g, name=name, value=value, cardinality=checkNotNone(_cardinality))
  
  
  def _edge_property(*, g: GraphTraversal, name: str, value: Any) -> GraphTraversal:
      return _property_or_drop(g=g, name=name, value=value, cardinality=None)
  
  
  def _property_or_drop(*, g: GraphTraversal, name: str, value: Any, cardinality: Optional[Cardinality]) \
          -> GraphTraversal:
      """
      You want to use _vertex_property or _edge_property.
      """
      if value is None:
          return g.sideEffect(__.properties(name).drop())
      else:
          # complicated: edges can't have cardinality supplied and are implied to be single
          p = __.property(name, value) if cardinality is None else __.property(cardinality, name, value)
          return _append_traversal(g, p)
  
  
  def _V(label: Union[str, VertexTypes, VertexType], key: Optional[Union[str, TextP]],
         key_property_name: Optional[str] = None, g: Optional[Union[Traversal, GraphTraversalSource]] = None,
         **properties: Any) -> GraphTraversal:
      """
      Gets a vertex with the given label and key and returns the valueMap.  If no such vertex exists, None is
      returned.  (If more than one exists, which would be super surprising, an exception is raised.)
      """
      if g is None:
          g = __.start()
      properties = dict(properties or {})
      if isinstance(key, str):
          id = ensure_vertex_type(label).id(key=key)
          g = g.V(id)
      elif key is not None:
          # let's support predicates on the key, but need to limit it to either the test_shard (or unsharded perhaps)
  
          if key_property_name is None:
              raise AssertionError('expected key_property_name')
          g = g.V().has(get_label_from(label), key_property_name, key)
          if get_shard():
              properties.setdefault(WellKnownProperties.TestShard.value.name, get_shard())
      else:
          # let's support hasLabel, but need to limit it to either the test_shard (or unsharded perhaps)
          g = g.V().hasLabel(get_label_from(label))
          if get_shard():
              properties.setdefault(WellKnownProperties.TestShard.value.name, get_shard())
  
      # should we do this when using the V(id)? there are a couple or one case where we use it to filter  so seems handy
      if properties is not None:
          for name, value in properties.items():
              if value is not None:
                  # you can have value be a predicate, like within('foo','bar') or such
                  g = g.has(name, value)
              else:
                  g = g.hasNot(name)
  
      return g
  
  
  def _has(*, g: Traversal, label: Union[None, str, VertexTypes, EdgeTypes, VertexType, EdgeType],
           key: Optional[str], key_property_name: Optional[str] = 'key',
           properties: Optional[Mapping[str, Any]] = None) -> GraphTraversal:
      """
      Matches a vertex or edge with the given label and key
      """
  
      if (label is None) and (key is None) and (properties is None):
          raise AssertionError('at least label or key or properties must be present')
      if label is not None:
          _label = get_label_from(label)
          if key is None:
              g = g.hasLabel(_label)
          else:
              if key_property_name is None:
                  raise AssertionError('must supply key_property_name if supplying key')
              # you can have key be a predicate, like within('foo','bar') or such
              g = g.has(_label, key_property_name, key)
      elif key is not None:
          if key_property_name is None:
              raise AssertionError('must supply key_property_name if supplying key')
          g = g.has(key_property_name, key)
  
      # don't usually need to blend in the shard for test isolation (since _has almost always is used at the end of a
      # traversal rooted in one component)
  
      if properties is not None:
          for name, value in properties.items():
              if value is not None:
                  # you can have value be a predicate, like within('foo','bar') or such
                  g = g.has(name, value)
              else:
                  g = g.hasNot(name)
  
      return g
  
  
  def _upsert(*, executor: ExecuteQuery, execute: Callable[[ResultSet], TYPE] = FromResultSet.getOnly,
              g: GraphTraversalSource, label: Union[VertexTypes, VertexType], key_property_name: str, key: str,
              traversal_if_exists: Optional[Traversal] = None, traversal_if_add: Optional[Traversal] = None,
              traversal: Optional[Traversal] = __.id(), **properties: Any) -> TYPE:
  
      if not isinstance(label, (VertexTypes, VertexType)):
          raise AssertionError(f'expected label to be a VertexType or VertexTypes: {label}')
      if isinstance(label, VertexTypes):
          id = label.value.id(key=key, **properties)
      elif isinstance(label, VertexType):
          id = label.id(key=key, **properties)
      else:
          raise AssertionError('wat')  # appease mypy
      if get_shard():
          properties.setdefault(WellKnownProperties.TestShard.value.name, get_shard())
  
      existing_node = executor(query=g.V(id).valueMap(True), get=FromResultSet.getOptional)
      _label = get_label_from(label)
  
      if existing_node is None:
          add = __.addV(_label).property(T.id, id).property(Cardinality.single, key_property_name, key)
          set_properties = _properties_or_drop_except(label, properties) if properties else None
          g = _append_traversal(g, add, traversal_if_add, set_properties, traversal)
      else:
          # get existing vertex...
          get = __.V(id)
          set_properties = _properties_or_drop_if_changed_except(
              label=label, thing=properties, existing=existing_node) if properties else None
          g = _append_traversal(g, get, traversal_if_exists, set_properties, traversal)
  
      # return the result
      return executor(query=g, get=execute)
  
  
  def _link(*, executor: ExecuteQuery, execute: Optional[Callable[[ResultSet], TYPE]] = None,  # noqa: C901
            g: GraphTraversalSource, edge_label: Union[EdgeTypes, EdgeType], key_property_name: Optional[str] = None,
            vertex1_label: Optional[Union[str, VertexTypes]] = None, vertex1_key: Optional[str] = None,
            vertex2_label: Optional[Union[str, VertexTypes]] = None, vertex2_key: Optional[str] = None,
            vertex1_id: Optional[Any] = None, vertex2_id: Optional[Any] = None,
            edge_properties: Dict[str, Any] = {}, traversal_if_exists: Optional[Traversal] = None,
            traversal_if_add: Optional[Traversal] = None, traversal: Optional[Traversal] = None,
            **properties: Any) -> Optional[TYPE]:
      """
      Creates an edge from vertex 1 to vertex 2 with a given label, if such
      an edge does not exist.  It does not create vertex1 or vertex 2, so if
      they don't exist, you will get some kind of error (usually an interation error).
  
      :param edge_properties: A map of edge properties and values to be used for
                              edge idempotence. If an unexpired edge with these
                              properties and values exists, no edge will be created.
  
      """
  
      if (vertex1_label is not None and vertex1_key is not None) == (vertex1_id is not None):
          raise AssertionError(f'pass either vertex1_label and vertex1_key or vertex1_id, but not both')
      if (vertex2_label is not None and vertex2_key is not None) == (vertex2_id is not None):
          raise AssertionError(f'pass either vertex2_label and vertex2_key or vertex2_id, but not both')
  
      # Return the raw id of a vertex
      if vertex1_id is None:
          if (key_property_name is None) or (vertex1_label is None):
              raise AssertionError(f'expected both key_property_name and vertex1_label')
          # we used to query to find this, but now that we're deterministically generating them we can do:
          vertex1_id = ensure_vertex_type(vertex1_label).id(**{key_property_name: vertex1_key})
          # but let's query to ensure it exists (which the previous pattern also ensured
          executor(query=g.V(vertex1_id), get=FromResultSet.getOnly)
  
      if vertex2_id is None:
          if (key_property_name is None) or (vertex2_label is None):
              raise AssertionError(f'expected both key_property_name and vertex2_label')
          vertex2_id = ensure_vertex_type(vertex2_label).id(**{key_property_name: vertex2_key})
          executor(query=g.V(vertex2_id), get=FromResultSet.getOnly)
  
      if (vertex1_id is None) or (vertex2_id is None):
          raise AssertionError(f'vertex1_label={vertex1_label}, vertex1_key={vertex1_key} \
                                 or vertex2_label={vertex2_label}, vertex2_key={vertex2_key} either does not exist! \
                                 vertex1_id={vertex1_id}, vertex2_id={vertex2_id}')
  
      _label = get_label_from(edge_label)
  
      edge_traversal = g.V(vertex1_id).outE(_label).where(__.inV().hasId(vertex2_id))
      for key, value in edge_properties.items():
          edge_traversal = edge_traversal.has(key, value)
      edge_id = executor(query=edge_traversal.id(), get=FromResultSet.getOptional)
  
      # If no link exists, create one!
      if edge_id is None:
          g = g.V(vertex1_id).as_('one').V(vertex2_id).addE(_label).from_('one').property('created', timestamp())
          g = _append_traversal(g, traversal_if_add)
      else:
          # Just get the edge in case we need to add properties
          g = _append_traversal(g.E(edge_id), traversal_if_exists)
  
      if properties:
          g = _append_traversal(g, _properties_or_drop_except(edge_label, properties))
  
      # updated properties, etc.
      if traversal is not None:
          g = _append_traversal(g, traversal)
  
      # return the result
      return executor(query=g, get=execute if execute is not None else FromResultSet.getOnly)
  
  
  def _expire_other_links(  # noqa: C901
          *, executor: ExecuteQuery, g: GraphTraversalSource,
          edge_label: EdgeTypes, key_property_name: Optional[str] = None,
          vertex1_label: Optional[Union[str, VertexTypes]] = None, vertex1_key: Optional[str] = None,
          vertex2_label: Optional[Union[str, VertexTypes]] = None, vertex2_key: Optional[str] = None,
          vertex1_id: Optional[Union[int, str, P]] = None, vertex2_id: Optional[Union[int, str, P]] = None,
          edge_direction: Direction = Direction.OUT,
          **properties: Any) -> None:
      """
      Expires edges of a given label other than the ones connecting
      vertex 1 and 2. If keys are not specified, will expire all edges between the two
      vertex types
      """
  
      if (vertex1_label is not None) == (vertex1_id is not None):
          raise AssertionError('pass either vertex1_label and vertex1_key or vertex1_id, but not both')
      if (vertex2_label is not None) == (vertex2_id is not None):
          raise AssertionError('pass either vertex2_label and vertex2_key or vertex2_id, but not both')
  
      # Return the raw id of a vertex
      if vertex1_id is None:
          if (key_property_name is None) or (vertex1_label is None):
              raise AssertionError(f'expected both key_property_name and vertex1_label')
          # we used to query to find this, but now that we're deterministically generating them we can do:
          vertex1_id = ensure_vertex_type(vertex1_label).id(**{key_property_name: vertex1_key})
          # but let's query to ensure it exists (which the previous pattern also ensured
          executor(query=g.V(vertex1_id), get=FromResultSet.getOnly)
  
      if vertex2_id is None and vertex2_key is not None:
          # TODO support getting more than one here
          if (key_property_name is None) or (vertex2_label is None):
              raise AssertionError(f'expected both key_property_name and vertex2_label')
          vertex2_id = ensure_vertex_type(vertex2_label).id(**{key_property_name: vertex2_key})
          executor(query=g.V(vertex2_id), get=FromResultSet.getOnly)
  
      if vertex1_id is not None:
          g = g.V(vertex1_id)
      else:
          if vertex1_label is None:
              raise AssertionError(f'expected vertex1_label')
          g = g.V().hasLabel(get_label_from(vertex1_label))
  
      # Traverse the edges from vertex 1
      if edge_direction == Direction.OUT:
          g = g.outE(get_label_from(edge_label))
      else:
          g = g.inE(get_label_from(edge_label))
      g = g
  
      # Find edges to vertexes with the label but an id that isn't vertex 2
      _filter = __.inV() if edge_direction == Direction.OUT else __.outV()
      if vertex2_id is None:
          if vertex2_label is None:
              raise AssertionError(f'expected vertex2_label')
          g = g.filter(_filter)
      elif isinstance(vertex2_id, (int, str)):
          g = g.filter(_append_traversal(_filter, __.id().is_(P.neq(vertex2_id))))
      elif isinstance(vertex2_id, P):
          g = g.filter(_append_traversal(_filter, __.id().is_(not_(vertex2_id))))
      g = g.drop()
      executor(g, get=FromResultSet.iterate)
  
  
  def _expire_link(*, executor: ExecuteQuery, g: GraphTraversalSource,
                   key_property_name: str, edge_label: Union[EdgeTypes, EdgeType],
                   vertex1_label: Optional[Union[str, VertexTypes, VertexType]] = None,
                   vertex1_key: Optional[Union[str, P]] = None,
                   vertex1_id: Optional[Union[int, str, P]] = None,
                   vertex2_label: Optional[Union[str, VertexTypes, VertexType]] = None,
                   vertex2_key: Optional[Union[str, P]] = None,
                   vertex2_id: Optional[Union[int, str, P]] = None) -> None:
      """
      Expires link between two vertices. Refer to vertices either by supplying
      label + key, or by providing the gremlin vertex ids directly
      """
      if (vertex1_label is not None and vertex1_key is not None) == (vertex1_id is not None):
          raise AssertionError(f'pass either vertex1_label and vertex1_key or vertex1_id, but not both')
      if (vertex2_label is not None and vertex2_key is not None) == (vertex2_id is not None):
          raise AssertionError(f'pass either vertex2_label and vertex2_key or vertex2_id, but not both')
  
      # Return the raw ids of the vertices
      if vertex1_id is None:
          if (key_property_name is None) or (vertex1_label is None):
              raise AssertionError(f'expected both key_property_name and vertex1_label')
          # we used to query to find this, but now that we're deterministically generating them we can do:
          vertex1_id = ensure_vertex_type(vertex1_label).id(**{key_property_name: vertex1_key})
          # but let's query to ensure it exists (which the previous pattern also ensured
          executor(query=g.V(vertex1_id), get=FromResultSet.getOnly)
  
      if vertex2_id is None:
          if (key_property_name is None) or (vertex2_label is None):
              raise AssertionError(f'expected both key_property_name and vertex2_label')
          # we used to query to find this, but now that we're deterministically generating them we can do:
          vertex2_id = ensure_vertex_type(vertex2_label).id(**{key_property_name: vertex2_key})
          # but let's query to ensure it exists (which the previous pattern also ensured
          executor(query=g.V(vertex2_id), get=FromResultSet.getOnly)
  
      if (vertex1_id is None) or (vertex2_id is None) or (vertex1_id == vertex2_id):
          raise AssertionError(f'pass either vertex1_label and vertex1_key or vertex1_id, but not both')
  
      g = g.V(vertex1_id)
      g = g.bothE(get_label_from(edge_label)).where(bothV().hasId(vertex2_id))
  