text: import logging
  from collections import defaultdict
  
  from ..order.orderpackage import OrderPackageType, BetfairOrderPackage
  from ..events import events
  from ..exceptions import ControlError, OrderError
  from ..utils import chunks, get_market_notes
  
  logger = logging.getLogger(__name__)
  
  
  class Transaction:
      """
      Process place, cancel, update and replace requests.
  
      Default behaviour is to execute immediately however
      when it is used as a context manager requests can
      be batched, for example:
  
          with market.transaction() as t:
              market.place_order(order)  # executed immediately in separate transaction
              t.place_order(order)  # executed on transaction __exit__
  
          with market.transaction() as t:
              t.place_order(order)
              ..
              t.execute()  # above order executed
              ..
              t.cancel_order(order)
              t.place_order(order)  # both executed on transaction __exit__
      """
  
      def __init__(self, market, id_: int, async_place_orders: bool):
          self.market = market
          self._id = id_  # unique per market only
          self._async_place_orders = async_place_orders
          self._pending_orders = False
          self._pending_place = []  # list of (<Order>, market_version)
          self._pending_cancel = []  # list of (<Order>, None)
          self._pending_update = []  # list of (<Order>, None)
          self._pending_replace = []  # list of (<Order>, market_version)
  
      def place_order(
          self,
          order,
          market_version: int = None,
          execute: bool = True,
          force: bool = False,
      ) -> bool:
          if (
              execute
              and not force
              and self._validate_controls(order, OrderPackageType.PLACE) is False
          ):
              return False
          # place
          order.place(
              self.market.market_book.publish_time,
              market_version,
              self._async_place_orders,
          )
          if self.market.blotter.has_trade(order.trade.id):
              new_trade = False
          else:
              new_trade = True
          if order.id not in self.market.blotter:
              self.market.blotter[order.id] = order
              # update market_notes
              market_notes = get_market_notes(self.market, order.selection_id)
              order.market_notes = market_notes
              if order.trade.market_notes is None:
                  order.trade.market_notes = market_notes
          else:
              raise OrderError("Order %s has already been placed" % order.id)
          if new_trade:
              self.market.flumine.log_control(events.TradeEvent(order.trade))
          if execute:  # handles replaceOrder
              runner_context = order.trade.strategy.get_runner_context(*order.lookup)
              runner_context.place(order.trade.id)
              self._pending_place.append((order, market_version))
              self._pending_orders = True
          return True
  
      def cancel_order(
          self, order, size_reduction: float = None, force: bool = False
      ) -> bool:
          if (
              not force
              and self._validate_controls(order, OrderPackageType.CANCEL) is False
          ):
              return False
          # cancel
          order.cancel(size_reduction)
          self._pending_cancel.append((order, None))
          self._pending_orders = True
          return True
  
      def update_order(
          self, order, new_persistence_type: str, force: bool = False
      ) -> bool:
          if (
              not force
              and self._validate_controls(order, OrderPackageType.UPDATE) is False
          ):
              return False
          # update
          order.update(new_persistence_type)
          self._pending_update.append((order, None))
          self._pending_orders = True
          return True
  
      def replace_order(
          self, order, new_price: float, market_version: int = None, force: bool = False
      ) -> bool:
          if (
              not force
              and self._validate_controls(order, OrderPackageType.REPLACE) is False
          ):
              return False
          # replace
          order.replace(new_price)
          self._pending_replace.append((order, market_version))
          self._pending_orders = True
          return True
  
      def execute(self) -> int:
          packages = []
          if self._pending_place:
              packages += self._create_order_package(
                  self._pending_place,
                  OrderPackageType.PLACE,
                  async_=self._async_place_orders,
              )
          if self._pending_cancel:
              packages += self._create_order_package(
                  self._pending_cancel,
                  OrderPackageType.CANCEL,
              )
          if self._pending_update:
              packages += self._create_order_package(
                  self._pending_update,
                  OrderPackageType.UPDATE,
              )
          if self._pending_replace:
              packages += self._create_order_package(
                  self._pending_replace,
                  OrderPackageType.REPLACE,
              )
          if packages:
              for package in packages:
                  self.market.flumine.process_order_package(package)
              if logger.isEnabledFor(logging.INFO):
                  logger.info(
                      "%s order packages executed in transaction" % len(packages),
                      extra={
                          "market_id": self.market.market_id,
                          "order_packages": [o.info for o in packages],
                          "transaction_id": self._id,
                      },
                  )
              self._pending_orders = False
          return len(packages)
  
      def _validate_controls(self, order, package_type: OrderPackageType) -> bool:
          # return False on violation
          try:
              for control in self.market.flumine.trading_controls:
                  control(order, package_type)
              for control in self.market.flumine.client.trading_controls:
                  control(order, package_type)
          except ControlError:
              return False
          else:
              return True
  
      def _create_order_package(
          self, orders: list, package_type: OrderPackageType, async_: bool = False
      ) -> list:
          # group orders by marketVersion
          orders_grouped = defaultdict(list)
          for o in orders:
              orders_grouped[o[1]].append(o[0])
          # create packages (chunked by limit)
          limit = BetfairOrderPackage.order_limit(package_type)
          packages = []
          for market_version, package_orders in orders_grouped.items():
              for chunked_orders in chunks(package_orders, limit):
                  packages.append(
                      BetfairOrderPackage(
                          client=self.market.flumine.client,
                          market_id=self.market.market_id,
                          orders=chunked_orders,
                          package_type=package_type,
                          bet_delay=self.market.market_book.bet_delay,
                          market_version=market_version,
                          async_=async_,
                      )
                  )
          orders.clear()
          return packages
  
      def __enter__(self):
          return self
  
      def __exit__(self, exc_type, exc_val, exc_tb):
          if self._pending_orders:
              self.execute()
  