Repository: digital-asset/dazl-client (language: python)

Issue:
python: Move dazl.model.core.ContractMatch to dazl.query.
Continuing to move more code out of `dazl.models`.

This code will actually remain (and possibly be expanded) in v8; query filtering is one area where the HTTP JSON API provides a richer API than the gRPC Ledger API, and in general some primitive client-side filtering has proved useful in the past.


Discussion / hints:

Fix patch:
diff --git a/VERSION b/VERSION
index 437feeff..ba7f754d 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-7.4.0rc2
+7.4.0
diff --git a/python/dazl/cli/metadata.py b/python/dazl/cli/metadata.py
index c0384fa6..42042fa8 100644
--- a/python/dazl/cli/metadata.py
+++ b/python/dazl/cli/metadata.py
@@ -10,10 +10,11 @@
 
 from .. import LOG, Network
 from ..client.config import AnonymousNetworkConfig, configure_parser
+from ..damlast import DarFile
+from ..damlast.lookup import MultiPackageLookup
+from ..damlast.protocols import SymbolLookup
 from ..model.core import ConnectionTimeoutError, UserTerminateRequest
-from ..model.types_store import PackageStore
 from ..pretty import PrettyOptions, get_pretty_printer
-from ..util.dar_repo import LocalDarRepository
 from ._base import CliCommand
 
 
@@ -45,9 +46,11 @@ def execute(self, args) -> int:
 
     @staticmethod
     def execute_static_metadata(files: Collection[str], options: PrettyOptions) -> int:
-        repo = LocalDarRepository()
-        repo.add_source(*files)
-        _process_metadata(repo.store, options)
+        lookup = MultiPackageLookup()
+        for file in files:
+            with DarFile(file) as dar_file:
+                lookup.add_archive(*dar_file.archives())
+        _process_metadata(lookup, options)
         return 0
 
     @staticmethod
@@ -65,11 +68,11 @@ def execute_runtime_metadata(config: "AnonymousNetworkConfig", options: PrettyOp
 
 
 async def _main(network: Network, options):
-    metadata = await network.aio_global().metadata()
-    _process_metadata(metadata.store, options)
+    await network.aio_global().metadata()
+    _process_metadata(network.lookup, options)
 
 
-def _process_metadata(store: PackageStore, options: PrettyOptions) -> None:
+def _process_metadata(lookup: SymbolLookup, options: PrettyOptions) -> None:
     import sys
 
     if sys.stdout.isatty():
@@ -85,7 +88,7 @@ def _process_metadata(store: PackageStore, options: PrettyOptions) -> None:
         pygments = None
         formatter = None
 
-    pretty_printer = get_pretty_printer(options.format, options, store)
+    pretty_printer = get_pretty_printer(options.format, options, lookup)
     if pretty_printer is None:
         raise RuntimeError(f"unknown format: {options.format}")
 
diff --git a/python/dazl/client/_network_client_impl.py b/python/dazl/client/_network_client_impl.py
index 722924e6..4bf9f6c4 100644
--- a/python/dazl/client/_network_client_impl.py
+++ b/python/dazl/client/_network_client_impl.py
@@ -170,16 +170,21 @@ async def aio_run(self, *coroutines) -> None:
         with AioLoopPerfMonitor(self._metrics.loop_responsiveness):
             if config.server_port is not None:
                 LOG.info("Opening port %s for metrics...", config.server_port)
-                from aiohttp import web
+                try:
+                    from aiohttp import web
 
-                from ..server import get_app
+                    from ..server import get_app
 
-                app = get_app(self)
-                app_runner = web.AppRunner(app)
-                await app_runner.setup()
-                site = web.TCPSite(app_runner, config.server_host, config.server_port)
-                ensure_future(site.start())
-                LOG.info("Listening on %s:%s for metrics.", config.server_host, config.server_port)
+                    app = get_app(self)
+                    app_runner = web.AppRunner(app)
+                    await app_runner.setup()
+                    site = web.TCPSite(app_runner, config.server_host, config.server_port)
+                    ensure_future(site.start())
+                    LOG.info(
+                        "Listening on %s:%s for metrics.", config.server_host, config.server_port
+                    )
+                except ImportError:
+                    LOG.warning("Could not start metrics because aiohttp is not installed")
             else:
                 LOG.info(
                     "No server_port configuration was specified, so metrics and other stats "
diff --git a/python/dazl/client/_party_client_impl.py b/python/dazl/client/_party_client_impl.py
index 1ef4832e..983113c4 100644
--- a/python/dazl/client/_party_client_impl.py
+++ b/python/dazl/client/_party_client_impl.py
@@ -19,18 +19,13 @@
     Set,
     Tuple,
     TypeVar,
+    Union,
 )
 import uuid
 
 from .. import LOG
 from ..damlast.daml_lf_1 import PackageRef, TypeConName
-from ..model.core import (
-    ContractContextualData,
-    ContractContextualDataCollection,
-    ContractMatch,
-    ContractsState,
-    Party,
-)
+from ..model.core import ContractContextualData, ContractContextualDataCollection, ContractsState
 from ..model.ledger import LedgerMetadata
 from ..model.network import OAuthSettings, connection_settings
 from ..model.reading import (
@@ -48,8 +43,9 @@
     TransactionStartEvent,
 )
 from ..model.writing import CommandBuilder, CommandDefaults, CommandPayload, EventHandlerResponse
-from ..prim import ContractId, TimeDeltaLike, to_timedelta
+from ..prim import ContractId, Party, TimeDeltaLike, to_timedelta
 from ..protocols import LedgerClient, LedgerNetwork
+from ..query import ContractMatch
 from ..scheduler import Invoker
 from ..util.asyncio_util import ServiceQueue, completed, named_gather
 from ..util.prim_natural import n_things
@@ -67,12 +63,19 @@
 T = TypeVar("T")
 
 
+__all__ = ["_PartyClientImpl"]
+
+
 class _PartyClientImpl:
+    invoker: Invoker
+    party: Party
+    bots: BotCollection
+
     def __init__(self, parent: "_NetworkImpl", party: "Party"):
         self.parent = parent
         self.metrics = parent._metrics
-        self.invoker = parent.invoker  # type: Invoker
-        self.party = party  # type: Party
+        self.invoker = parent.invoker
+        self.party = party
 
         self._config_values = dict()  # type: Dict[str, Any]
         self._config = None  # type: Optional[_NetworkConfig]
@@ -212,7 +215,7 @@ async def emit_ready(
 
     # region Active/Historical Contract Set management
 
-    def find_by_id(self, cid: ContractId) -> "Optional[ContractContextualData]":
+    def find_by_id(self, cid: Union[str, ContractId]) -> "Optional[ContractContextualData]":
         return self._acs.get(cid)
 
     def find(
@@ -535,10 +538,8 @@ async def main_writer(self):
         Main coroutine for submitting commands.
         """
         LOG.info("Writer loop for party %s is starting...", self.party)
-        ledger_fut = ensure_future(self._pool.ledger())
-
-        client = self._client_fut.result()  # type: LedgerClient
-        metadata = ledger_fut.result()  # type: LedgerMetadata
+        client = await self._client_fut  # type: LedgerClient
+        metadata = await self._pool.ledger()  # type: LedgerMetadata
         validator = ValidateSerializer(self.parent.lookup)
 
         self._writer.pending_commands.start()
diff --git a/python/dazl/client/api.py b/python/dazl/client/api.py
index 8eab1010..1b5db879 100644
--- a/python/dazl/client/api.py
+++ b/python/dazl/client/api.py
@@ -19,7 +19,7 @@
 
 from asyncio import Future, ensure_future, get_event_loop
 from contextlib import contextmanager
-from functools import wraps
+from functools import partial, wraps
 from logging import INFO
 from pathlib import Path
 from typing import Any, Awaitable, BinaryIO, Collection, List, Optional, Tuple, Union
@@ -34,7 +34,6 @@
 from ..model.core import (
     ContractContextualData,
     ContractContextualDataCollection,
-    ContractMatch,
     ContractsState,
     Dar,
 )
@@ -53,6 +52,7 @@
 from ..model.types import TemplateNameLike
 from ..model.writing import EventHandlerResponse
 from ..prim import ContractData, ContractId, Party, TimeDeltaLike, to_party
+from ..query import ContractMatch, is_match
 from ..scheduler import RunLevel, validate_install_signal_handlers
 from ..util.asyncio_util import await_then
 from ..util.io import get_bytes
@@ -70,6 +70,18 @@
 from .bots import Bot, BotCollection
 from .config import AnonymousNetworkConfig, NetworkConfig, PartyConfig
 
+__all__ = [
+    "DEFAULT_TIMEOUT_SECONDS",
+    "simple_client",
+    "async_network",
+    "Network",
+    "GlobalClient",
+    "AIOGlobalClient",
+    "SimpleGlobalClient",
+    "PartyClient",
+    "AIOPartyClient",
+    "SimplePartyClient",
+]
 DEFAULT_TIMEOUT_SECONDS = 30
 
 
@@ -407,7 +419,7 @@ def bots(self) -> "Collection[Bot]":
             DeprecationWarning,
             stacklevel=2,
         )
-        return self._impl.bots()
+        return self._impl.bots
 
     def __enter__(self):
         """
@@ -697,8 +709,10 @@ def add_ledger_created(
         :param match:
             An (optional) parameter that filters the templates to be received by the callback.
         """
+        filter_fn = partial(is_match, match) if match is not None else None
+
         bot = self._impl.bots.add_new(party_client=self, name=handler.__name__)
-        bot.add_event_handler(EventKey.contract_created(True, template), handler, match)
+        bot.add_event_handler(EventKey.contract_created(True, template), handler, filter_fn)
         return bot
 
     def ledger_exercised(
@@ -777,8 +791,10 @@ def add_ledger_archived(
         :param match:
             An (optional) parameter that filters the templates to be received by the callback.
         """
+        filter_fn = partial(is_match, match) if match is not None else None
+
         for key in EventKey.contract_archived(True, template):
-            self._impl.add_event_handler(key, handler, match, self)
+            self._impl.add_event_handler(key, handler, filter_fn, self)
 
     # </editor-fold>
 
@@ -1300,8 +1316,10 @@ def _background_ledger_contract_create(
         ) -> "Awaitable[EventHandlerResponse]":
             return self._impl.invoker.run_in_executor(lambda: handler(event))
 
+        filter_fn = partial(is_match, match) if match is not None else None
+
         for key in EventKey.contract_created(True, template):
-            self._impl.add_event_handler(key, _background_ledger_contract_create, match, self)
+            self._impl.add_event_handler(key, _background_ledger_contract_create, filter_fn, self)
 
     def ledger_exercised(
         self, template: "Any", choice: str
@@ -1393,8 +1411,10 @@ def _background_ledger_contract_archived(
         ) -> "Awaitable[EventHandlerResponse]":
             return self._impl.invoker.run_in_executor(lambda: handler(event))
 
+        filter_fn = partial(is_match, match) if match is not None else None
+
         for key in EventKey.contract_archived(True, template):
-            self._impl.add_event_handler(key, _background_ledger_contract_archived, match, self)
+            self._impl.add_event_handler(key, _background_ledger_contract_archived, filter_fn, self)
 
     # </editor-fold>
 
diff --git a/python/dazl/client/pkg_loader.py b/python/dazl/client/pkg_loader.py
index 4cb2c076..975c0c05 100644
--- a/python/dazl/client/pkg_loader.py
+++ b/python/dazl/client/pkg_loader.py
@@ -5,7 +5,7 @@
 from concurrent.futures import ThreadPoolExecutor
 from datetime import timedelta
 import sys
-from typing import AbstractSet, Awaitable, Callable, Dict, Set, TypeVar
+from typing import AbstractSet, Awaitable, Callable, Dict, Optional, Set, TypeVar
 
 from .. import LOG
 from ..damlast.daml_lf_1 import Archive, Package, PackageRef
@@ -53,11 +53,11 @@ def __init__(
         self,
         package_lookup: "MultiPackageLookup",
         conn: "SyncPackageService" = None,
-        timeout: "timedelta" = DEFAULT_TIMEOUT,
+        timeout: "Optional[timedelta]" = DEFAULT_TIMEOUT,
     ):
         self._package_lookup = package_lookup
         self._conn = conn
-        self._timeout = timeout
+        self._timeout = timeout or DEFAULT_TIMEOUT
         self._loading_futs = dict()  # type: Dict[PackageRef, Awaitable[Package]]
         self._parsing_futs = dict()  # type: Dict[PackageRef, Awaitable[Archive]]
         self._executor = ThreadPoolExecutor(3)
diff --git a/python/dazl/client/state.py b/python/dazl/client/state.py
index 4eea69d8..3ef9f7e4 100644
--- a/python/dazl/client/state.py
+++ b/python/dazl/client/state.py
@@ -6,18 +6,17 @@
 from typing import Awaitable, Collection, Dict, List, Optional, Union, cast
 import warnings
 
-from ..client._reader_match import is_match
 from ..damlast.daml_lf_1 import TypeConName
 from ..damlast.protocols import SymbolLookup
 from ..model.core import (
     ContractContextualData,
     ContractContextualDataCollection,
-    ContractMatch,
     ContractsState,
     UnknownTemplateWarning,
 )
 from ..model.reading import ContractArchiveEvent, ContractCreateEvent
 from ..prim import ContractId
+from ..query import ContractMatch, is_match
 from ..scheduler import Invoker
 from ..util.asyncio_util import await_then
 
diff --git a/python/dazl/damlast/expand.py b/python/dazl/damlast/expand.py
index 5de751e8..79eab895 100644
--- a/python/dazl/damlast/expand.py
+++ b/python/dazl/damlast/expand.py
@@ -175,7 +175,7 @@ def visit_expr_app(self, app: "Expr.App"):
             if builtin is not None:
                 type_args = app.fun.ty_app.types
                 new_args = [self.visit_expr(arg) for arg in app.args]
-                result = builtin().simplify(type_args, new_args)
+                result = builtin.simplify(type_args, new_args)
                 if result is not None:
                     return result
 
diff --git a/python/dazl/damlast/pb_parse.py b/python/dazl/damlast/pb_parse.py
index 72bfa034..6621319d 100644
--- a/python/dazl/damlast/pb_parse.py
+++ b/python/dazl/damlast/pb_parse.py
@@ -15,6 +15,7 @@ def __init__(self, current_package: "PackageRef"):
         from typing import List
 
         self.current_package = current_package
+        self.current_module = None  # type: Optional[ModuleRef]
         self.interned_strings = []  # type: List[str]
         self.interned_dotted_names = []  # type: List[Sequence[str]]
         self.interned_types = []  # type: List[Type]
@@ -41,14 +42,20 @@ def parse_ModuleRef(self, pb) -> "Optional[ModuleRef]":
             raise ValueError(f"unknown sum type value: {sum_name!r}")
 
     def parse_TypeConName(self, pb) -> "TypeConName":
+        module_ref = self.parse_ModuleRef(pb.module)
+        if module_ref is None:
+            raise ValueError("missing a valid ModuleRef in a TypeConName definition")
         return TypeConName(
-            self.parse_ModuleRef(pb.module),
+            module_ref,
             self._resolve_dotted_name(pb.name_dname, pb.name_interned_dname).segments,
         )
 
     def parse_TypeSynName(self, pb) -> "TypeSynName":
+        module_ref = self.parse_ModuleRef(pb.module)
+        if module_ref is None:
+            raise ValueError("missing a valid ModuleRef in a TypeSynName definition")
         return TypeSynName(
-            self.parse_ModuleRef(pb.module),
+            module_ref,
             self._resolve_dotted_name(pb.name_dname, pb.name_interned_dname).segments,
         )
 
@@ -56,8 +63,11 @@ def parse_DottedName(self, pb) -> "DottedName":
         return DottedName(segments=self._resolve_string_seq(pb.segments, pb.segments_interned_id))
 
     def parse_ValName(self, pb) -> "ValName":
+        module_ref = self.parse_ModuleRef(pb.module)
+        if module_ref is None:
+            raise ValueError("missing a valid ModuleRef in a ValName definition")
         return ValName(
-            self.parse_ModuleRef(pb.module),
+            module_ref,
             self._resolve_string_seq(pb.name_dname, pb.name_interned_dname),
         )
 
@@ -518,7 +528,17 @@ def parse_Scenario_EmbedExpr(self, pb) -> "Scenario.EmbedExpr":
         return Scenario.EmbedExpr(type=self.parse_Type(pb.type), body=self.parse_Expr(pb.body))
 
     def parse_Location(self, pb) -> "Location":
-        return Location(self.parse_ModuleRef(pb.module), self.parse_Location_Range(pb.range))
+        module_ref = self.parse_ModuleRef(pb.module)
+        if module_ref is None:
+            module_ref = self.current_module
+        if module_ref is None:
+            # This is probably a programming mistake, since nowhere in the Daml-LF protobuf does
+            # a Location occur outside of a Module
+            raise ValueError(
+                "cannot parse a Location object without a ModuleRef outside of a Module"
+            )
+
+        return Location(module_ref, self.parse_Location_Range(pb.range))
 
     def parse_Location_Range(self, pb) -> "Location.Range":
         return Location.Range(pb.start_line, pb.start_col, pb.end_line, pb.end_col)
@@ -648,14 +668,21 @@ def parse_FeatureFlags(self, pb) -> "FeatureFlags":
         )
 
     def parse_Module(self, pb) -> "Module":
-        return Module(
-            name=self._resolve_dotted_name(pb.name_dname, pb.name_interned_dname),
-            flags=self.parse_FeatureFlags(pb.flags),
-            synonyms=tuple(self.parse_DefTypeSyn(value) for value in pb.synonyms),
-            data_types=tuple(self.parse_DefDataType(data_type) for data_type in pb.data_types),
-            values=tuple(self.parse_DefValue(value) for value in pb.values),
-            templates=tuple(self.parse_DefTemplate(template) for template in pb.templates),
-        )
+        name = self._resolve_dotted_name(pb.name_dname, pb.name_interned_dname)
+        self.current_module = ModuleRef(self.current_package, name)
+        try:
+            module = Module(
+                name=name,
+                flags=self.parse_FeatureFlags(pb.flags),
+                synonyms=tuple(self.parse_DefTypeSyn(value) for value in pb.synonyms),
+                data_types=tuple(self.parse_DefDataType(data_type) for data_type in pb.data_types),
+                values=tuple(self.parse_DefValue(value) for value in pb.values),
+                templates=tuple(self.parse_DefTemplate(template) for template in pb.templates),
+            )
+        finally:
+            self.current_module = None
+
+        return module
 
     def parse_Package(self, pb) -> "Package":
         # TODO: this modifies state in a parser which is less than ideal; a better pattern would be
diff --git a/python/dazl/damleval/expr_eval.py b/python/dazl/damleval/expr_eval.py
deleted file mode 100644
index 98263681..00000000
--- a/python/dazl/damleval/expr_eval.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-# SPDX-License-Identifier: Apache-2.0
-
-
-from dataclasses import dataclass
-from datetime import date, datetime, timedelta, timezone
-from decimal import Decimal
-from typing import Any, Union
-
-from ..damlast import ExprVisitor, IdentityTypeVisitor
-from ..damlast.builtins import builtins
-from ..damlast.daml_lf_1 import (
-    Block,
-    BuiltinFunction,
-    Case,
-    Expr,
-    PrimCon,
-    PrimLit,
-    Scenario,
-    Type,
-    Update,
-    ValName,
-)
-from ..prim import FrozenDict, Party
-
-
-@dataclass(frozen=True)
-class Constant:
-    value: Any
-
-
-class ExpressionEvaluator(ExprVisitor[Any], IdentityTypeVisitor):
-    """
-    Compute the result of an expression.
-    """
-
-    def visit_expr(self, expr: "Union[Expr, Constant]"):
-        if isinstance(expr, Constant):
-            return expr.value
-        else:
-            return super(ExpressionEvaluator, self).visit_expr(expr)
-
-    # noinspection PyUnusedLocal,PyMethodMayBeStatic
-    def visit_constant(self, constant: Constant) -> "Any":
-        return constant.value
-
-    def visit_expr_var(self, var: str) -> "Any":
-        pass
-
-    def visit_expr_val(self, val: "ValName") -> "Any":
-        pass
-
-    def visit_expr_builtin(self, builtin: "BuiltinFunction") -> "Any":
-        return builtins.resolve(builtin).evaluate()
-
-    def visit_expr_prim_con(self, prim_con: "PrimCon") -> "Any":
-        if PrimCon.CON_TRUE == prim_con:
-            return True
-        elif PrimCon.CON_FALSE == prim_con:
-            return False
-        elif PrimCon.CON_UNIT == prim_con:
-            return {}
-        else:
-            raise ValueError(f"unknown PrimCon value: {prim_con}")
-
-    def visit_expr_prim_lit(self, prim_lit: "PrimLit") -> "Any":
-        if prim_lit.int64 is not None:
-            return prim_lit.int64
-        elif prim_lit.decimal is not None:
-            return Decimal(prim_lit.decimal)
-        elif prim_lit.text is not None:
-            return prim_lit.text
-        elif prim_lit.timestamp is not None:
-            return datetime.fromtimestamp(prim_lit.timestamp / 1e9, timezone.utc)
-        elif prim_lit.party is not None:
-            return Party(prim_lit.party)
-        elif prim_lit.date is not None:
-            return date(1970, 1, 1) + timedelta(days=prim_lit.date)
-        else:
-            raise ValueError(f"unknown PrimLit value: {prim_lit}")
-
-    def visit_expr_rec_con(self, rec_con: "Expr.RecCon") -> "Any":
-        return FrozenDict({fwt.field: self.visit_expr(fwt.expr) for fwt in rec_con.fields})
-
-    def visit_expr_rec_proj(self, rec_proj: "Expr.RecProj") -> "Any":
-        record_expr = self.visit_expr(rec_proj.record)
-        return record_expr[rec_proj.field]
-
-    def visit_expr_variant_con(self, variant_con: "Expr.VariantCon") -> "Any":
-        ctor = variant_con.variant_con
-        value = self.visit_expr(variant_con.variant_arg)
-        return FrozenDict({ctor: value})
-
-    def visit_expr_struct_con(self, struct_con: "Expr.StructCon") -> "Any":
-        return FrozenDict({fwt.field: self.visit_expr(fwt.expr) for fwt in struct_con.fields})
-
-    def visit_expr_struct_proj(self, struct_proj: "Expr.StructProj") -> "Any":
-        tuple_expr = self.visit_expr(struct_proj.tuple)
-        return tuple_expr[struct_proj.field]
-
-    def visit_expr_app(self, app: "Expr.App") -> "Any":
-        fun = self.visit_expr(app.fun)
-        args = [self.visit_expr(arg) for arg in app.args]
-        return fun.apply(*args)
-
-    def visit_expr_ty_app(self, ty_app: "Expr.TyApp") -> "Any":
-        pass
-
-    def visit_expr_abs(self, abs_: "Expr.Abs") -> "Any":
-        pass
-
-    def visit_expr_ty_abs(self, ty_abs: "Expr.TyAbs") -> "Any":
-        pass
-
-    def visit_expr_case(self, case: "Case") -> "Any":
-        pass
-
-    def visit_expr_let(self, let: "Block") -> "Any":
-        current_scope = self
-        for binding in let.bindings:
-            var = binding.binder.var
-            value = current_scope.eval_Expr(binding.bound)
-            current_scope = current_scope.child_evaluator(value_bindings={var: Constant(value)})
-        return current_scope.eval_Expr(let.body)
-
-    def visit_expr_nil(self, nil: "Expr.Nil") -> "Any":
-        pass
-
-    def visit_expr_cons(self, cons: "Expr.Cons") -> "Any":
-        pass
-
-    def visit_expr_update(self, update: "Update") -> "Any":
-        raise RuntimeError("expressions cannot currently be evaluated in the Update monad")
-
-    def visit_expr_scenario(self, scenario: "Scenario") -> "Any":
-        raise RuntimeError("expressions cannot currently be evaluated in the Scenario monad")
-
-    def visit_expr_rec_upd(self, rec_upd: "Expr.RecUpd") -> "Any":
-        raise RuntimeError("expressions cannot currently be evaluated in the Update monad")
-
-    def visit_expr_struct_upd(self, struct_upd: "Expr.StructUpd") -> "Any":
-        raise RuntimeError("expressions cannot currently be evaluated in the Update monad")
-
-    def visit_expr_optional_none(self, optional_none: "Expr.OptionalNone") -> "Any":
-        return None
-
-    def visit_expr_optional_some(self, optional_some: "Expr.OptionalSome") -> "Any":
-        return self.visit_expr(optional_some.body)
-
-    def visit_type_var(self, var: "Type.Var") -> "Type":
-        resolved_type = self.resolve_type(var.var)
-        if resolved_type is not None:
-            return self.visit_type(resolved_type)
-        else:
-            return Type(var=var)
diff --git a/python/dazl/damleval/type_eval.py b/python/dazl/damleval/type_eval.py
deleted file mode 100644
index 2346d988..00000000
--- a/python/dazl/damleval/type_eval.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-# SPDX-License-Identifier: Apache-2.0
-# type: ignore
-# ^ This file will be removed in dazl v8. It mostly doesn't work.
-
-from typing import Optional
-
-from ..damlast.daml_lf_1 import (
-    BuiltinFunction,
-    Expr,
-    FieldWithType,
-    PrimCon,
-    PrimLit,
-    PrimType,
-    Type,
-    ValName,
-)
-from ..damlast.visitor import ExprVisitor, IdentityTypeVisitor
-from ..model.types_store import PackageStore
-from ..util.typing import safe_cast
-
-
-class TypeComputer(ExprVisitor[Type], IdentityTypeVisitor):
-    """
-    Evaluate the :class:`Type` of an :class:`Expr`.
-    """
-
-    def __init__(self, store: "PackageStore"):
-        self.store = safe_cast(PackageStore, store)
-
-    def resolve_type(self, var: str) -> "Optional[Type]":
-        return None
-
-    def visit_expr_var(self, var: str) -> "Type":
-        pass
-
-    def visit_expr_val(self, val: "ValName") -> "Type":
-        vref = self.store.resolve_value_reference(val)
-        if vref is not None:
-            return self.visit_expr(vref)
-        else:
-            raise RuntimeError(f"unknown val name: {val}")
-
-    def visit_expr_builtin(self, builtin: "BuiltinFunction") -> "Type":
-        pass
-
-    def visit_expr_prim_con(self, prim_con: "PrimCon") -> "Type":
-        if PrimCon.CON_TRUE == prim_con or PrimCon.CON_FALSE == prim_con:
-            return Type(prim=Type.Prim(PrimType.BOOL, args=()))
-        else:
-            return Type(prim=Type.Prim(PrimType.UNIT, args=()))
-
-    def visit_expr_prim_lit(self, prim_lit: "PrimLit") -> "Type":
-        if prim_lit.int64 is not None:
-            return Type(prim=Type.Prim(PrimType.INT64, args=()))
-        elif prim_lit.date is not None:
-            return Type(prim=Type.Prim(PrimType.DATE, args=()))
-        elif prim_lit.timestamp is not None:
-            return Type(prim=Type.Prim(PrimType.TIMESTAMP, args=()))
-        elif prim_lit.decimal is not None:
-            return Type(prim=Type.Prim(PrimType.DECIMAL, args=()))
-        elif prim_lit.party is not None:
-            return Type(prim=Type.Prim(PrimType.PARTY, args=()))
-        elif prim_lit.text is not None:
-            return Type(prim=Type.Prim(PrimType.TEXT, args=()))
-        else:
-            raise RuntimeError("unknown prim lit type")
-
-    def visit_expr_rec_con(self, rec_con: "Expr.RecCon") -> "Type":
-        return self.visit_type(Type(con=rec_con.tycon))
-
-    def visit_expr_rec_proj(self, rec_proj: "Expr.RecProj") -> "Type":
-        rec_type = self.visit_type_con(rec_proj.tycon)
-        old_type = self.store.resolve_type_reference(rec_type.con.tycon)
-        for n, v in old_type.as_args_list():
-            if n == rec_proj.field:
-                return get_new_type(v)
-
-    def visit_expr_variant_con(self, variant_con: "Expr.VariantCon") -> "Type":
-        return self.visit_type(Type(con=variant_con.tycon))
-
-    def visit_expr_struct_con(self, struct_con: "Expr.StructCon") -> "Type":
-        return Type(
-            tuple=Type.Tuple(
-                [FieldWithType(fwe.field, self.visit_expr(fwe.expr)) for fwe in struct_con.fields]
-            )
-        )
-
-    def visit_expr_struct_proj(self, struct_proj: "Expr.StructProj") -> "Type":
-        pass
-
-    def visit_expr_app(self, app: "Expr.App") -> "Type":
-        fun_type = self.visit_expr(app.fun)
-        if fun_type.prim.prim == PrimType.ARROW:
-            t = fun_type
-            arg_count = len(app.args)
-            while arg_count > 0:
-                arg_count -= 1
-                t = t.prim.args[1]
-            return t
-        else:
-            raise ValueError(f"unexpected type of app body: {fun_type}")
-
-    def visit_expr_ty_app(self, ty_app: "Expr.TyApp") -> "Type":
-        core_type = self.visit_expr(ty_app.expr)
-        if ty_app.types:
-
-            def unexpected_type_error(*_, **__):
-                raise RuntimeError("Unexpected tyapp")
-
-            return core_type.Sum_match(
-                lambda var: Type(var=Type.Var(var=var.var, args=var.args + ty_app.types)),
-                lambda con: Type(con=Type.Con(tycon=con.tycon, args=con.args + ty_app.types)),
-                lambda prim: Type(prim=Type.Prim(prim=prim.prim, args=prim.args + ty_app.types)),
-                lambda forall: unexpected_type_error,
-                lambda tuple: unexpected_type_error,
-            )
-        return core_type
-
-    def visit_expr_abs(self, abs_: "Expr.Abs") -> "Type":
-        t = self.visit_expr(abs_.body)
-        for param in reversed(abs_.param):
-            t = arrow_type(param.type, t)
-        return t
-
-    def visit_expr_ty_abs(self, ty_abs: "Expr.TyAbs") -> "Type":
-        return "..."
-        # scope = self.with_type_var() ty_abs.param[0].var
-        # return scope.visit_expr(ty_abs.body)
-
-    def visit_expr_case(self, case: "Case") -> "Type":
-        # TODO: Probably not right
-        return self.visit_expr(case.alts[0].body)
-
-    def visit_expr_let(self, let: "Block") -> "Type":
-        return self.visit_expr(let.body)
-
-    def visit_expr_nil(self, nil: "Expr.Nil") -> "Type":
-        return Type(prim=Type.Prim(PrimType.LIST, [self.visit_type(nil.type)]))
-
-    def visit_expr_cons(self, cons: "Expr.Cons") -> "Type":
-        return Type(prim=Type.Prim(PrimType.LIST, [self.visit_type(cons.type)]))
-
-    def visit_expr_update(self, update: "Update") -> "Type":
-        raise Exception("Update not yet supported")
-
-    def visit_expr_scenario(self, scenario: "Scenario") -> "Type":
-        raise Exception("Scenario not yet supported")
-
-    def visit_expr_rec_upd(self, rec_upd: "Expr.RecUpd") -> "Type":
-        raise Exception("Update not yet supported")
-
-    def visit_expr_struct_upd(self, struct_upd: "Expr.StructUpd") -> "Type":
-        raise Exception("Update not yet supported")
-
-    def visit_expr_optional_none(self, optional_none: "Expr.OptionalNone") -> "Type":
-        return Type(prim=Type.Prim(PrimType.OPTIONAL, [self.visit_type(optional_none.type)]))
-
-    def visit_expr_optional_some(self, optional_some: "Expr.OptionalSome") -> "Type":
-        return Type(prim=Type.Prim(PrimType.OPTIONAL, [self.visit_type(optional_some.type)]))
-
-    def visit_type_var(self, var: "Type.Var") -> "Type":
-        resolved_type = self.resolve_type(var.var)
-        if resolved_type is not None:
-            return self.visit_type(resolved_type)
-        else:
-            return Type(var=var)
-
-
-def arrow_type(input: "Type", output: "Type") -> "Type":
-    return Type(prim=Type.Prim(PrimType.ARROW, (input, output)))
diff --git a/python/dazl/model/core.py b/python/dazl/model/core.py
index 79340f0f..53b1db40 100644
--- a/python/dazl/model/core.py
+++ b/python/dazl/model/core.py
@@ -13,28 +13,40 @@
 """
 from dataclasses import dataclass
 from datetime import datetime
-from pathlib import Path
-from typing import (
-    TYPE_CHECKING,
-    BinaryIO,
-    Callable,
-    Collection,
-    Dict,
-    Optional,
-    Tuple,
-    TypeVar,
-    Union,
-)
+from typing import TYPE_CHECKING, Collection, Dict, Optional, Tuple, TypeVar, Union
 import warnings
 
 from ..damlast.daml_lf_1 import TypeConName
+from ..damlast.pkgfile import Dar
 from ..prim import ContractData, ContractId as ContractId_, Party
+from ..query import ContractMatch
 
 if TYPE_CHECKING:
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", DeprecationWarning)
         from .types import Type, TypeReference
 
+__all__ = [
+    "ConfigurationError",
+    "ConnectionTimeoutError",
+    "ContractContextualData",
+    "ContractContextualDataCollection",
+    "ContractData",
+    "ContractId",
+    "ContractMatch",
+    "ContractsHistoricalState",
+    "ContractsState",
+    "Dar",
+    "DazlError",
+    "DazlImportError",
+    "DazlPartyMissingError",
+    "DazlWarning",
+    "Party",
+    "ProcessDiedException",
+    "UnknownTemplateWarning",
+    "UserTerminateRequest",
+]
+
 T = TypeVar("T")
 
 
@@ -127,7 +139,6 @@ def for_json(self):
         return self.value
 
 
-ContractMatch = Union[None, Callable[[ContractData], bool], ContractData]
 ContractsState = Dict[ContractId, ContractData]
 ContractsHistoricalState = Dict[ContractId, Tuple[ContractData, bool]]
 
@@ -161,10 +172,6 @@ class ContractContextualData:
     active: bool
 
 
-# Wherever the API expects a DAR, we can take a file path, `bytes`, or a byte buffer.
-Dar = Union[bytes, str, Path, BinaryIO]
-
-
 @dataclass(frozen=True)
 class SourceLocation:
     file_name: "Optional[str]"
@@ -222,6 +229,13 @@ class CommandTimeoutError(DazlError):
     Raised when a corresponding event for a command was not seen in the appropriate time window.
     """
 
+    def __init__(self):
+        warnings.warn(
+            "This error is never raised; this symbol will be removed in dazl v8",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
 
 class ConfigurationError(DazlError):
     """
@@ -249,6 +263,13 @@ class ConnectionClosedError(DazlError):
     closed.
     """
 
+    def __init__(self):
+        warnings.warn(
+            "This error is never raised; this symbol will be removed in dazl v8",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+
 
 class UnknownTemplateWarning(DazlWarning):
     """
diff --git a/python/dazl/model/definition.py b/python/dazl/model/definition.py
deleted file mode 100644
index 69f24433..00000000
--- a/python/dazl/model/definition.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-# SPDX-License-Identifier: Apache-2.0
-from typing import Optional, Sequence
-
-from ..damlast.daml_lf_1 import Expr, FieldWithType
-from .types import ModuleRef
-
-
-class DamlDeclaration:
-    """"""
-
-    module: ModuleRef
-    name: Sequence[str]
-
-
-class DamlDataType(DamlDeclaration):
-    """"""
-
-    module: ModuleRef
-    name: Sequence[str]
-    fields: "Sequence[FieldWithType]"
-    eq_instance: "Optional[Expr]"
-    ord_instance: "Optional[Expr]"
-    show_instance: "Optional[Expr]"
-
-
-class DamlTemplate(DamlDataType):
-    signatories: "Expr"
-    observers: "Expr"
-    agreement: "Expr"
-    precondition: "Expr"
-    choices: "Sequence[DamlChoice]"
-
-
-class DamlChoice:
-    name: str
diff --git a/python/dazl/model/types.py b/python/dazl/model/types.py
index a5ab121b..d536dcad 100644
--- a/python/dazl/model/types.py
+++ b/python/dazl/model/types.py
@@ -849,6 +849,9 @@ def error(_: Any) -> Any:
         raise Exception()
 
     def annotate_path(t: Union[RecordType, VariantType]) -> Tuple[TypeEvaluationContext, Type]:
+        if t.name is None:
+            raise ValueError("cannot annotate a path with an anonymous type")
+
         return context.append_path(t.name), t
 
     return type_dispatch_table(
diff --git a/python/dazl/model/writing.py b/python/dazl/model/writing.py
index 71c5c40b..0e7bf76a 100644
--- a/python/dazl/model/writing.py
+++ b/python/dazl/model/writing.py
@@ -338,13 +338,11 @@ def append(self, *commands: CommandsOrCommandSequence) -> "CommandBuilder":
         else:
             return self.append_nonatomically(*commands)
 
-    def append_atomically(self, *commands: Union[Command, Sequence[Command]]) -> "CommandBuilder":
+    def append_atomically(self, *commands: CommandsOrCommandSequence) -> "CommandBuilder":
         self._commands.extend([flatten_command_sequence(commands)])
         return self
 
-    def append_nonatomically(
-        self, *commands: Union[Command, Sequence[Command]]
-    ) -> "CommandBuilder":
+    def append_nonatomically(self, *commands: CommandsOrCommandSequence) -> "CommandBuilder":
         self._commands.extend([[cmd] for cmd in flatten_command_sequence(commands)])
         return self
 
diff --git a/python/dazl/pretty/__init__.py b/python/dazl/pretty/__init__.py
index 49a78218..3fffb470 100644
--- a/python/dazl/pretty/__init__.py
+++ b/python/dazl/pretty/__init__.py
@@ -13,8 +13,8 @@
 
 from typing import TYPE_CHECKING, Optional, Type
 
-from ..model.types_store import PackageStore
-from ._render_base import PrettyPrintBase, pretty_print_syntax
+from ..damlast.protocols import SymbolLookup
+from ._render_base import PrettyPrintBase, pretty_print_syntax  # type: ignore
 from .options import PrettyOptions
 from .render_csharp import CSharpPrettyPrint  # type: ignore
 from .render_daml import DEFAULT_PRINTER as DAML_PRETTY_PRINTER, DamlPrettyPrinter  # type: ignore
@@ -25,6 +25,9 @@
     from .pygments_daml_lexer import DAMLLexer as _DAMLLexer_TYPE
 
 
+__all__ = ["get_pretty_printer", "DAMLLexer", "ALL_PRINTER_TYPES", "PrettyOptions"]
+
+
 def _import_daml_lexer() -> "Optional[Type[_DAMLLexer_TYPE]]":
     # pygments isn't absolutely required, but if it's loaded, also provide our lexer
     try:
@@ -43,9 +46,9 @@ def _import_daml_lexer() -> "Optional[Type[_DAMLLexer_TYPE]]":
 
 # noinspection PyShadowingBuiltins,PyShadowingNames
 def get_pretty_printer(
-    format: str, options: "PrettyOptions", store: "PackageStore"
+    format: str, options: "PrettyOptions", lookup: "SymbolLookup"
 ) -> "Optional[PrettyPrintBase]":
     for printer in ALL_PRINTER_TYPES:
         if printer.syntax.startswith(format):
-            return printer(store, options)
+            return printer(lookup, options)
     return None
diff --git a/python/dazl/pretty/_render_base.py b/python/dazl/pretty/_render_base.py
index 5ccb8a18..748ce4bf 100644
--- a/python/dazl/pretty/_render_base.py
+++ b/python/dazl/pretty/_render_base.py
@@ -1,5 +1,6 @@
 # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
+# type: ignore
 
 from io import StringIO
 import json
@@ -30,11 +31,19 @@
 from ..damlast.protocols import SymbolLookup
 from ..damlast.util import package_local_name, unpack_arrow_type
 from ..damlast.visitor import ExprVisitor, ModuleVisitor, PackageVisitor, TypeVisitor
-from ..model.definition import DamlDataType, DamlTemplate
 from ..prim import to_date, to_datetime
 from .options import PrettyOptions
 from .util import is_hidden_module_name, maybe_parentheses
 
+__all__ = [
+    "PrettyPrintBase",
+    "CodeContext",
+    "decode_special_chars",
+    "get_pretty_printer_type",
+    "register_pretty_printer",
+    "pretty_print_syntax",
+]
+
 
 # noinspection PyMethodMayBeStatic
 class PrettyPrintBase(PackageVisitor[str], ModuleVisitor[str], ExprVisitor[str], TypeVisitor[str]):
@@ -215,7 +224,7 @@ def visit_def_data_type(self, def_data_type: "DefDataType") -> str:
         return self.visit_def_template(None, def_data_type)
 
     def visit_def_template(
-        self, template: "DefTemplate", def_data_type: "Optional[DefDataType]" = None
+        self, template: "Optional[DefTemplate]", def_data_type: "Optional[DefDataType]" = None
     ) -> str:
         """
         Attempt to render the :class:`DefTemplate`. If this method successfully returns, then the
@@ -227,21 +236,6 @@ def visit_def_template(
         """
         raise NotImplementedError
 
-    def visit_daml_data_type(self, data_type: DamlDataType) -> str:
-        """
-        Render the specified data type.
-
-        :param data_type:
-        :return:
-        """
-        raise NotImplementedError
-
-    def visit_daml_template(self, template: DamlTemplate) -> str:
-        """
-        Render a template instance.
-        """
-        raise NotImplementedError
-
     def visit_def_value(self, def_value: "DefValue") -> str:
         scope = self.with_decl(def_value.name_with_type.name, def_value.name_with_type.type)
         return scope.visit_expr(def_value.expr)
@@ -562,9 +556,9 @@ def visit_expr_ty_abs(self, ty_abs: "Expr.TyAbs") -> "str":
         return self.with_type_abs(ty_abs.param).visit_expr(ty_abs.body)
 
     def visit_expr_case(self, case: "Case") -> "str":
-        return None
+        raise NotImplementedError("visit_expr_case needs an implementation")
 
-    def visit_expr_casealt(self, alt: "CaseAlt", type: "Optional[Type]" = None) -> str:
+    def visit_expr_casealt(self, alt: "CaseAlt") -> str:
         pattern_text = alt.Sum_match(
             self.visit_expr_casealt_default,
             self.visit_expr_casealt_variant,
@@ -596,10 +590,10 @@ def visit_expr_casealt_variant(self, variant: "CaseAlt.Variant"):
     def visit_expr_casealt_prim_con(self, prim_con: "PrimCon"):
         return self.visit_expr_prim_con(prim_con)
 
-    def visit_expr_casealt_nil(self, nil: "Unit", type: "Optional[Type]" = None):
+    def visit_expr_casealt_nil(self, nil: "Unit", type: "Type" = None):
         return self.visit_expr_nil(Expr.Nil(type))
 
-    def visit_expr_casealt_cons(self, cons: "CaseAlt.Cons", type: "Optional[Type]" = None):
+    def visit_expr_casealt_cons(self, cons: "CaseAlt.Cons", type: "Type" = None):
         return self.visit_expr_cons(
             Expr.Cons(front=(Expr(var=cons.var_head),), tail=Expr(var=cons.var_tail), type=type)
         )
diff --git a/python/dazl/pretty/render_csharp.py b/python/dazl/pretty/render_csharp.py
index 3677091b..0d6c1e72 100644
--- a/python/dazl/pretty/render_csharp.py
+++ b/python/dazl/pretty/render_csharp.py
@@ -4,8 +4,6 @@
 
 from typing import Mapping, Optional, Sequence, Union
 
-from dazl.model.definition import DamlTemplate
-
 from ..damlast.daml_lf_1 import (
     Block,
     BuiltinFunction,
diff --git a/python/dazl/pretty/table/fmt_pretty.py b/python/dazl/pretty/table/fmt_pretty.py
index a722117a..5c190a9f 100644
--- a/python/dazl/pretty/table/fmt_pretty.py
+++ b/python/dazl/pretty/table/fmt_pretty.py
@@ -49,7 +49,7 @@ def render(
             header_row = ["", "#cid", *(expand_record_field_names(context, Type.Con(name, ())))]
 
             with LOG.debug_timed("Render entries as strings"):
-                rows = [
+                orig_rows = [
                     (
                         sort.key(entry),
                         [
@@ -62,8 +62,8 @@ def render(
                 ]
 
             with LOG.debug_timed("Sort rows"):
-                rows.sort(key=lambda val: val[0])
-                rows = [row for _, row in rows]
+                orig_rows.sort(key=lambda val: val[0])
+                rows = [row for _, row in orig_rows]
                 rows.insert(0, header_row)
 
             with LOG.debug_timed("Measure entry sizes"):
diff --git a/python/dazl/protocols/_base.py b/python/dazl/protocols/_base.py
index a1d80eb0..99ee2dc7 100644
--- a/python/dazl/protocols/_base.py
+++ b/python/dazl/protocols/_base.py
@@ -10,7 +10,7 @@
 from typing import Optional, Sequence, Union
 
 from .. import LOG
-from ..damlast.protocols import SymbolLookup
+from ..damlast.lookup import MultiPackageLookup
 from ..model.ledger import LedgerMetadata
 from ..model.network import HTTPConnectionSettings
 from ..model.reading import BaseEvent, ContractFilter, TransactionFilter
@@ -22,7 +22,7 @@
 
 @dataclass(frozen=True)
 class LedgerConnectionOptions:
-    lookup: "SymbolLookup"
+    lookup: "MultiPackageLookup"
     connect_timeout: "Optional[timedelta]"
     package_lookup_timeout: "Optional[timedelta]"
     eager_package_fetch: bool
diff --git a/python/dazl/protocols/autodetect.py b/python/dazl/protocols/autodetect.py
index fcb9bc71..8d830b1d 100644
--- a/python/dazl/protocols/autodetect.py
+++ b/python/dazl/protocols/autodetect.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 from datetime import datetime
+from functools import partial
 from threading import Event, RLock, Thread
 from typing import Awaitable, Dict, Iterable, Optional, Union
 
@@ -35,8 +36,8 @@ def __init__(self, invoker: "Invoker", options: "LedgerConnectionOptions"):
         self._main_thread = Thread(target=self._main, daemon=True)
         self._main_thread.start()
 
-    def ledger(self) -> "Awaitable[LedgerMetadata]":
-        return self._ledger_future
+    async def ledger(self) -> "LedgerMetadata":
+        return await self._ledger_future
 
     async def connect_anonymous(
         self, settings: "HTTPConnectionSettings", context_path: "Optional[str]"
@@ -71,24 +72,25 @@ async def connect(
             raise RuntimeError(f"Unknown protocol version: {ledger.protocol_version}")
 
     async def upload_package(self, dar_contents: bytes) -> None:
+        connection = self._first_connection
+        if connection is None:
+            raise RuntimeError("cannot upload a package until a connection has been established")
+
         from .v1.grpc import grpc_upload_package
 
-        return await self._invoker.run_in_executor(
-            lambda: grpc_upload_package(self._first_connection, dar_contents)
-        )
+        await self._invoker.run_in_executor(partial(grpc_upload_package, connection, dar_contents))
 
-    async def set_time(self, new_time: datetime):
-        ledger = await self.ledger()
-        if ledger.protocol_version == "v1":
-            from .v1.grpc import grpc_set_time
+    async def set_time(self, new_time: datetime) -> None:
+        connection = self._first_connection
+        if connection is None:
+            raise RuntimeError("cannot upload a package until a connection has been established")
 
-            return await self._invoker.run_in_executor(
-                lambda: grpc_set_time(self._first_connection, ledger.ledger_id, new_time)
-            )
-        elif ledger.protocol_version == "v0":
-            raise RuntimeError(f"Unsupported protocol version: {ledger.protocol_version}")
-        else:
-            raise RuntimeError(f"Unknown protocol version: {ledger.protocol_version}")
+        from .v1.grpc import grpc_set_time
+
+        ledger = await self.ledger()
+        return await self._invoker.run_in_executor(
+            partial(grpc_set_time, connection, ledger.ledger_id, new_time)
+        )
 
     async def close(self) -> None:
         with self._lock:
@@ -144,7 +146,9 @@ def _get_connection(
                 LOG.debug("Initializing a connection to %s...", settings)
                 stub = self._connections.get(settings)
                 if stub is None:
-                    stub = AutodetectConnection(self._invoker, settings, context_path)
+                    stub = AutodetectConnection(
+                        self._invoker, self._options, settings, context_path
+                    )
                     self._connections[settings] = stub
                 return stub
         except:  # noqa
diff --git a/python/dazl/protocols/oauth.py b/python/dazl/protocols/oauth.py
index 8207c4bd..72b437d9 100644
--- a/python/dazl/protocols/oauth.py
+++ b/python/dazl/protocols/oauth.py
@@ -29,6 +29,8 @@ async def oauth_flow(settings: OAuthSettings) -> OAuthSettings:
             "grant_type": "client_credentials",
         }
 
+        if settings.token_uri is None:
+            raise ValueError("missing a token URI")
         response = None
         try:
             response = requests.post(
diff --git a/python/dazl/protocols/v1/grpc.py b/python/dazl/protocols/v1/grpc.py
index c08c54b5..38ded6ec 100644
--- a/python/dazl/protocols/v1/grpc.py
+++ b/python/dazl/protocols/v1/grpc.py
@@ -20,6 +20,7 @@
 )
 
 from ... import LOG
+from ..._gen.com.daml.ledger.api.v1.transaction_service_pb2 import GetTransactionTreesResponse
 from ...damlast.daml_lf_1 import PackageRef
 from ...damlast.parse import parse_archive_payload
 from ...model.core import ConnectionTimeoutError, UserTerminateRequest
@@ -117,6 +118,7 @@ async def events(self, transaction_filter: "TransactionFilter") -> "Sequence[Bas
             lambda: self.connection.transaction_service.GetTransactions(request)
         )
 
+        tt_stream = None  # type: Optional[Iterable[GetTransactionTreesResponse]]
         if transaction_filter.templates is None:
             # Filtering by package must disable the ability to handle exercise nodes; we may want to
             # consider dropping client-side support for exercise events anyway because they are not
@@ -124,8 +126,6 @@ async def events(self, transaction_filter: "TransactionFilter") -> "Sequence[Bas
             tt_stream = await self.connection.invoker.run_in_executor(
                 lambda: self.connection.transaction_service.GetTransactionTrees(request)
             )
-        else:
-            tt_stream = None
 
         tx_stream = await tx_future
 
@@ -258,7 +258,6 @@ def grpc_main_thread(connection: "GRPCv1Connection", ledger_id: str) -> "Iterabl
                 LOG.warning("Package syncing raised an exception.", exc_info=True)
 
     LOG.debug("The gRPC monitor thread is now shutting down.")
-    yield None
 
 
 class GRPCPackageProvider(PackageProvider):
@@ -319,8 +318,8 @@ def should_load(p: str) -> bool:
                 metadatas_pb[package_id] = parse_archive_payload(package_id, archive_payload)
 
         adr = find_dependencies(metadatas_pb, loaded_package_ids)
-        for package_id, archive_payload in adr.sorted_archives.items():
-            store.register_all(parse_daml_metadata_pb(package_id, archive_payload))
+        for package_id, archive_bytes in adr.sorted_archives.items():
+            store.register_all(parse_daml_metadata_pb(package_id, archive_bytes))
 
         LOG.debug("grpc_package_sync ended.")
 
diff --git a/python/dazl/protocols/v1/pb_parse_event.py b/python/dazl/protocols/v1/pb_parse_event.py
index a434d650..2aca2829 100644
--- a/python/dazl/protocols/v1/pb_parse_event.py
+++ b/python/dazl/protocols/v1/pb_parse_event.py
@@ -61,7 +61,7 @@ class BaseEventDeserializationContext:
     def deserializer_context(self) -> "Context":
         return Context(DECODER, self.lookup)
 
-    def offset_event(self, time: datetime, offset: str) -> OffsetEvent:
+    def offset_event(self, time: Optional[datetime], offset: str) -> OffsetEvent:
         return OffsetEvent(
             self.client, self.party, time, self.ledger_id, self.lookup, self.store, offset
         )
@@ -345,10 +345,10 @@ def to_transaction_events(
 
     if tt_stream_pb is not None:
         for transaction_tree_pb in tt_stream_pb:
-            for transaction_pb in transaction_tree_pb.transactions:
-                tx_events = events_by_offset.get(transaction_pb.offset)
+            for tt_pb in transaction_tree_pb.transactions:
+                tx_events = events_by_offset.get(tt_pb.offset)
                 if tx_events is not None:
-                    tx_events[-1:-1] = from_transaction_tree(context, transaction_pb)
+                    tx_events[-1:-1] = from_transaction_tree(context, tt_pb)
 
     for tx_events in events_by_offset.values():
         events.extend(tx_events)
@@ -425,8 +425,11 @@ def to_transaction_chunk(
         workflow_id=tx_pb.workflow_id,
     )
 
-    contract_events = [to_event(t_context, evt_pb) for evt_pb in tx_pb.events]
-    contract_events = [evt for evt in contract_events if evt is not None]
+    contract_events = []  # type: List[OffsetEvent]
+    for evt_pb in tx_pb.events:
+        evt = to_event(t_context, evt_pb)
+        if evt is not None:
+            contract_events.append(evt)
 
     return [
         t_context.transaction_start_event(contract_events),
@@ -438,7 +441,7 @@ def to_transaction_chunk(
 def to_event(
     context: "Union[TransactionEventDeserializationContext, ActiveContractSetEventDeserializationContext]",
     evt_pb: "Union[event_pb2.Event, tx_pb2.TreeEvent]",
-) -> "Optional[BaseEvent]":
+) -> "Optional[OffsetEvent]":
     try:
         event_type = evt_pb.WhichOneof("event")
     except ValueError:
diff --git a/python/dazl/damleval/__init__.py b/python/dazl/query/__init__.py
similarity index 59%
rename from python/dazl/damleval/__init__.py
rename to python/dazl/query/__init__.py
index e18f28af..55a50f33 100644
--- a/python/dazl/damleval/__init__.py
+++ b/python/dazl/query/__init__.py
@@ -1,5 +1,6 @@
 # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
 
-from .expr_eval import ExpressionEvaluator
-from .type_eval import TypeComputer  # type: ignore
+__all__ = ["ContractMatch", "is_match"]
+
+from .query import ContractMatch, is_match
diff --git a/python/dazl/client/_reader_match.py b/python/dazl/query/query.py
similarity index 86%
rename from python/dazl/client/_reader_match.py
rename to python/dazl/query/query.py
index 10c94f41..146b4952 100644
--- a/python/dazl/client/_reader_match.py
+++ b/python/dazl/query/query.py
@@ -1,11 +1,13 @@
 # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
 
-"""
-This module contains functions for testing contract data against a match object.
-"""
+from typing import Callable, Union
 
-from ..model.core import ContractData, ContractMatch
+from ..prim import ContractData
+
+__all__ = ["ContractMatch", "is_match"]
+
+ContractMatch = Union[None, Callable[[ContractData], bool], ContractData]
 
 
 def is_match(predicate: ContractMatch, cdata: ContractData) -> bool:
diff --git a/python/dazl/scheduler/_invoker.py b/python/dazl/scheduler/_invoker.py
index 8fe7f4aa..77e5374b 100644
--- a/python/dazl/scheduler/_invoker.py
+++ b/python/dazl/scheduler/_invoker.py
@@ -11,7 +11,7 @@
 from typing import List
 import warnings
 
-from ..prim import to_timedelta
+from ..prim import TimeDeltaLike, to_timedelta
 from ..util.asyncio_util import execute_in_loop, safe_create_future
 from ._base import RunLevel
 
@@ -93,7 +93,7 @@ def set_context_as_current(self) -> None:
         if self.executor is None:
             self.executor = ThreadPoolExecutor()
 
-    def run_in_loop(self, func, timeout: float = 30.0):
+    def run_in_loop(self, func, timeout: TimeDeltaLike = 30.0):
         """
         Schedule a normal function or coroutine function to be run on the event loop, and block
         until the function has returned.
diff --git a/python/dazl/scheduler/_invoker.pyi b/python/dazl/scheduler/_invoker.pyi
index ad618e8b..b4cfba7c 100644
--- a/python/dazl/scheduler/_invoker.pyi
+++ b/python/dazl/scheduler/_invoker.pyi
@@ -19,6 +19,8 @@ from ..prim import TimeDeltaLike
 
 T = TypeVar("T", covariant=True)
 
+__all__ = ["Invoker"]
+
 class Invoker:
     level: RunLevel
     loop: Optional[AbstractEventLoop]
@@ -34,13 +36,11 @@ class Invoker:
     ) -> None: ...
     def set_context_as_current(self) -> None: ...
     @overload
-    def run_in_loop(self, func: Callable[[], Awaitable[None]], timeout: float = 30) -> None: ...
-    @overload
-    def run_in_loop(self, func: Callable[[], None], timeout: float = 30) -> None: ...
+    def run_in_loop(self, func: Callable[[], None], timeout: TimeDeltaLike = 30) -> None: ...
     @overload
-    def run_in_loop(self, func: Callable[[], Awaitable[T]], timeout: float = 30) -> T: ...
+    def run_in_loop(self, func: Callable[[], Awaitable[T]], timeout: TimeDeltaLike = 30) -> T: ...
     @overload
-    def run_in_loop(self, func: Callable[[], T], timeout: float = 30) -> T: ...
+    def run_in_loop(self, func: Callable[[], T], timeout: TimeDeltaLike = 30) -> T: ...
     def run_in_executor(self, func: "Callable[[], T]") -> Awaitable[T]: ...
     def install_signal_handlers(self) -> None: ...
     def handle_sigint(self) -> None: ...
diff --git a/python/dazl/util/asyncio_util.py b/python/dazl/util/asyncio_util.py
index edd5f151..5f6289b9 100644
--- a/python/dazl/util/asyncio_util.py
+++ b/python/dazl/util/asyncio_util.py
@@ -35,6 +35,7 @@
     Tuple,
     TypeVar,
     Union,
+    cast,
     no_type_check,
 )
 import warnings
@@ -399,7 +400,7 @@ def start(self) -> None:
         more than once has no effect.
         """
         if not self._service_fut.done():
-            existing_items = self._q  # type: List[Tuple[T, Future]]
+            existing_items = cast(list, self._q)  # type: List[Tuple[T, Future]]
             self._q = AQueue()
             for item in existing_items:
                 self._q.put_nowait(item)
@@ -428,7 +429,7 @@ async def next(self) -> Optional[T_co]:
             self._prev_fut.set_result(None)
             self._prev_fut = None
 
-        q = self._q  # type: AQueue[Tuple[T_co, Future]]
+        q = cast(AQueue, self._q)  # type: AQueue[Tuple[T_co, Future]]
         value, fut = await q.get()
         if value is None:
             fut.set_result(None)
diff --git a/python/dazl/util/config_meta.py b/python/dazl/util/config_meta.py
index 3e83db16..7220ae13 100644
--- a/python/dazl/util/config_meta.py
+++ b/python/dazl/util/config_meta.py
@@ -8,7 +8,7 @@
 from argparse import _ActionsContainer
 from dataclasses import Field, dataclass, field, fields
 import logging
-from typing import Any, Callable, FrozenSet, Optional, Sequence, Tuple
+from typing import Any, Callable, FrozenSet, Optional, Sequence, Tuple, no_type_check
 
 from ..model.core import Party
 
@@ -41,6 +41,7 @@ def config_field(
     )
 
 
+@no_type_check
 def config_fields(class_or_instance: Any) -> "Sequence[Tuple[Field, ConfigParameter]]":
     return [(fld, fld.metadata.get("dazl.config")) for fld in fields(class_or_instance)]
 
diff --git a/python/dazl/util/dar_repo.py b/python/dazl/util/dar_repo.py
index ba08a416..f2db5572 100644
--- a/python/dazl/util/dar_repo.py
+++ b/python/dazl/util/dar_repo.py
@@ -1,23 +1,31 @@
 # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
-
 from contextlib import ExitStack
+from os import PathLike, fspath
 from pathlib import Path
 import time
 from typing import List, Sequence, Union
+import warnings
 
 from .. import LOG
-from ..model.types_store import PackageStore
-from ..util.dar import DarFile
-from ..util.path_util import pathify
 
 
 class LocalDarRepository:
     def __init__(self):
+        warnings.warn(
+            "LocalDarRepository is deprecated; use PackageLookup instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         self._context = ExitStack()
         self._dar_paths = []  # type: List[Path]
         self._files = set()
-        self.store = PackageStore.empty()
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore", DeprecationWarning)
+
+            from ..model.types_store import PackageStore
+
+            self.store = PackageStore.empty()
 
     def _add_source(self, path: Path) -> None:
         ext = path.suffix.lower()
@@ -32,7 +40,12 @@ def _add_source(self, path: Path) -> None:
 
         elif ext == ".dar":
             dar_parse_start_time = time.time()
-            dar = self._context.enter_context(DarFile(path))
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore", DeprecationWarning)
+
+                from ..util.dar import DarFile
+
+                dar = self._context.enter_context(DarFile(path))
             dar_package = dar.read_metadata()
             self._dar_paths.append(path)
             self.store.register_all(dar_package)
@@ -43,7 +56,7 @@ def _add_source(self, path: Path) -> None:
             LOG.error("Unknown extension: %s", ext)
             raise ValueError(f"Unknown extension: {ext}")
 
-    def add_source(self, *files: Union[str, Path]) -> None:
+    def add_source(self, *files: Union[str, PathLike, Path]) -> None:
         """
         Add a source file (either a .daml file, .dalf file, or a .dar file).
 
@@ -52,7 +65,7 @@ def add_source(self, *files: Union[str, Path]) -> None:
         :param files: Files to add to the archive.
         """
         for file in files:
-            path = pathify(file).resolve(strict=True)
+            path = Path(fspath(file)).resolve(strict=True)
             if path not in self._files:
                 self._files.add(path)
                 self._add_source(path)
diff --git a/python/dazl/util/path_util.py b/python/dazl/util/path_util.py
index cedf524d..aa5d0352 100644
--- a/python/dazl/util/path_util.py
+++ b/python/dazl/util/path_util.py
@@ -1,18 +1,16 @@
 # Copyright (c) 2017-2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
-
-from os import PathLike
+from os import PathLike, fspath
 from pathlib import Path
 from typing import Union
+import warnings
+
+__all__ = ["pathify"]
 
 
 def pathify(path: "Union[str, Path, PathLike]") -> Path:
     """
     Convert an object that could be a :class:`Path` into a :class:`Path`.
     """
-    if isinstance(path, Path):
-        return path
-    elif isinstance(path, PathLike):
-        return Path(path)
-    else:
-        raise ValueError(f"path must be a str or Path (got {path!r} instead)")
+    warnings.warn("pathify is deprecated; use os.fspath instead", DeprecationWarning, stacklevel=2)
+    return Path(fspath(path))
diff --git a/python/dazl/util/tools.py b/python/dazl/util/tools.py
index d7f35b68..c040173e 100644
--- a/python/dazl/util/tools.py
+++ b/python/dazl/util/tools.py
@@ -23,7 +23,7 @@
 V = TypeVar("V")
 
 
-__all__ = ["boundary_iter"]
+__all__ = ["boundary_iter", "flatten", "as_list", "get_matches"]
 
 
 def boundary_iter(obj: Iterable[T]) -> Generator[Tuple[bool, T], None, None]:
diff --git a/python/dazl/values/context.py b/python/dazl/values/context.py
index af60f190..11424936 100644
--- a/python/dazl/values/context.py
+++ b/python/dazl/values/context.py
@@ -195,6 +195,38 @@ def convert_text_map(
             for key, value in elements.items()
         }
 
+    def convert_gen_map(
+        self,
+        key_type: "Type",
+        value_type: "Type",
+        elements: "Mapping[Any, Any]",
+        mapper: "Optional[ValueMapper]" = None,
+    ) -> "Dict[str, Any]":
+        """
+        Convert a _map_ of elements, each of an assumed type.
+
+        This is a convenience method for :class:`ValueMapper` implementations where DAML-LF TextMaps
+        are represented as Python dictionaries. Note that a :class:`ValueMapper` may choose to
+        implement TextMap type conversion in a completely different way instead of calling this
+        function, which may be necessary if a TextMap of DAML-LF values is encoded in a way other
+        than a Python dict (i.e., Protobuf).
+
+        :param key_type:
+            The :class:`Type` of the key.
+        :param value_type:
+            The :class:`Type` of the value.
+        :param elements:
+            A mapping of keys to values.
+        :param mapper:
+            If not ``None``, an alternate mapper to use when descending down the object.
+        """
+        return {
+            self.append_path("[i]", mapper=mapper)
+            .convert(key_type, key): self.append_path(key, mapper=mapper)
+            .convert(value_type, value)
+            for i, (key, value) in enumerate(elements.items())
+        }
+
     def convert_contract_id(self, element_type: "Type", contract_id: "Any") -> "ContractId":
         """
         Convert a contract ID string to a :class:`ContractId`.
@@ -321,6 +353,13 @@ def value_error(self, value: "Any", message: str) -> "NoReturn":
             f'value at {".".join(self.path)} has an invalid value: {value} ({message})'
         )
 
+    @property
+    def depth(self) -> int:
+        """
+        Return the number of components in the path of this context.
+        """
+        return len(self.path)
+
     def _replace_all_type_vars(
         self, type_vars: "Mapping[str, Type]", fields: "DefDataType.Fields"
     ) -> "DefDataType.Fields":
diff --git a/python/dazl/values/pysample_encoder.py b/python/dazl/values/pysample_encoder.py
index b1f332ef..9f7ca4e8 100644
--- a/python/dazl/values/pysample_encoder.py
+++ b/python/dazl/values/pysample_encoder.py
@@ -4,7 +4,8 @@
 from typing import Any
 
 from ..damlast.daml_lf_1 import DefDataType, Type as DamlType
-from .mapper import Context, ValueMapper, convert
+from .context import Context
+from .mapper import ValueMapper
 
 
 class PythonSampleEncoder(ValueMapper):
@@ -15,7 +16,7 @@ def _recurse(self, context: "Context", key, item_type):
         if context.depth <= self.max_depth:
             return "..."
         else:
-            return convert(self, context.append_path(key), item_type, None)
+            return context.append_path(key).convert(item_type, None)
 
     def data_record(
         self, context: "Context", dt: "DefDataType", record: "DefDataType.Fields", obj: "Any"
diff --git a/python/mypy.ini b/python/mypy.ini
index a6091de5..221578ac 100644
--- a/python/mypy.ini
+++ b/python/mypy.ini
@@ -1,5 +1,11 @@
 [mypy]
 
+[mypy-google.auth.transport.*]
+ignore_missing_imports = True
+
+[mypy-google.oauth2.credentials.*]
+ignore_missing_imports = True
+
 [mypy-google.protobuf.pyext.*]
 ignore_missing_imports = True
 
diff --git a/python/pyproject.toml b/python/pyproject.toml
index ef3b440e..aac8f276 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -3,7 +3,7 @@
 
 [tool.poetry]
 name = "dazl"
-version = "7.4.0rc2"
+version = "7.4.0"
 description = "high-level Ledger API client for DAML ledgers"
 license = "Apache-2.0"
 authors = ["Davin K. Tanabe <davin.tanabe@digitalasset.com>"]


Test patch:
diff --git a/python/tests/unit/test_matches.py b/python/tests/unit/test_matches.py
index 15d4808c..d912ae66 100644
--- a/python/tests/unit/test_matches.py
+++ b/python/tests/unit/test_matches.py
@@ -5,7 +5,7 @@
 This module contains tests for testing contract data against a match object.
 """
 
-from dazl.client._reader_match import is_match
+from dazl.query import is_match
 
 
 def test_match_of_partial_keys():