@@ -85,19 +85,96 @@ def __str__(self):
8585class MutationsExceptionGroup (BigtableExceptionGroup ):
8686 """
8787 Represents one or more exceptions that occur during a bulk mutation operation
88+
89+ Exceptions will typically be of type FailedMutationEntryError, but other exceptions may
90+ be included if they are raised during the mutation operation
8891 """
8992
9093 @staticmethod
91- def _format_message (excs : list [FailedMutationEntryError ], total_entries : int ):
92- entry_str = "entry" if total_entries == 1 else "entries"
93- plural_str = "" if len (excs ) == 1 else "s"
94- return f"{ len (excs )} sub-exception{ plural_str } (from { total_entries } { entry_str } attempted)"
94+ def _format_message (
95+ excs : list [Exception ], total_entries : int , exc_count : int | None = None
96+ ) -> str :
97+ """
98+ Format a message for the exception group
99+
100+ Args:
101+ - excs: the exceptions in the group
102+ - total_entries: the total number of entries attempted, successful or not
103+ - exc_count: the number of exceptions associated with the request
104+ if None, this will be len(excs)
105+ """
106+ exc_count = exc_count if exc_count is not None else len (excs )
107+ entry_str = "entry" if exc_count == 1 else "entries"
108+ return f"{ exc_count } failed { entry_str } from { total_entries } attempted."
109+
110+ def __init__ (
111+ self , excs : list [Exception ], total_entries : int , message : str | None = None
112+ ):
113+ """
114+ Args:
115+ - excs: the exceptions in the group
116+ - total_entries: the total number of entries attempted, successful or not
117+ - message: the message for the exception group. If None, a default message
118+ will be generated
119+ """
120+ message = (
121+ message
122+ if message is not None
123+ else self ._format_message (excs , total_entries )
124+ )
125+ super ().__init__ (message , excs )
126+ self .total_entries_attempted = total_entries
95127
96- def __init__ (self , excs : list [FailedMutationEntryError ], total_entries : int ):
97- super ().__init__ (self ._format_message (excs , total_entries ), excs )
128+ def __new__ (
129+ cls , excs : list [Exception ], total_entries : int , message : str | None = None
130+ ):
131+ """
132+ Args:
133+ - excs: the exceptions in the group
134+ - total_entries: the total number of entries attempted, successful or not
135+ - message: the message for the exception group. If None, a default message
136+ """
137+ message = (
138+ message if message is not None else cls ._format_message (excs , total_entries )
139+ )
140+ instance = super ().__new__ (cls , message , excs )
141+ instance .total_entries_attempted = total_entries
142+ return instance
98143
99- def __new__ (cls , excs : list [FailedMutationEntryError ], total_entries : int ):
100- return super ().__new__ (cls , cls ._format_message (excs , total_entries ), excs )
144+ @classmethod
145+ def from_truncated_lists (
146+ cls ,
147+ first_list : list [Exception ],
148+ last_list : list [Exception ],
149+ total_excs : int ,
150+ entry_count : int ,
151+ ) -> MutationsExceptionGroup :
152+ """
153+ Create a MutationsExceptionGroup from two lists of exceptions, representing
154+ a larger set that has been truncated. The MutationsExceptionGroup will
155+ contain the union of the two lists as sub-exceptions, and the error message
156+ describe the number of exceptions that were truncated.
157+
158+ Args:
159+ - first_list: the set of oldest exceptions to add to the ExceptionGroup
160+ - last_list: the set of newest exceptions to add to the ExceptionGroup
161+ - total_excs: the total number of exceptions associated with the request
162+ Should be len(first_list) + len(last_list) + number of dropped exceptions
163+ in the middle
164+ - entry_count: the total number of entries attempted, successful or not
165+ """
166+ first_count , last_count = len (first_list ), len (last_list )
167+ if first_count + last_count >= total_excs :
168+ # no exceptions were dropped
169+ return cls (first_list + last_list , entry_count )
170+ excs = first_list + last_list
171+ truncation_count = total_excs - (first_count + last_count )
172+ base_message = cls ._format_message (excs , entry_count , total_excs )
173+ first_message = f"first { first_count } " if first_count else ""
174+ last_message = f"last { last_count } " if last_count else ""
175+ conjunction = " and " if first_message and last_message else ""
176+ message = f"{ base_message } ({ first_message } { conjunction } { last_message } attached as sub-exceptions; { truncation_count } truncated)"
177+ return cls (excs , entry_count , message )
101178
102179
103180class FailedMutationEntryError (Exception ):
@@ -108,14 +185,17 @@ class FailedMutationEntryError(Exception):
108185
109186 def __init__ (
110187 self ,
111- failed_idx : int ,
188+ failed_idx : int | None ,
112189 failed_mutation_entry : "RowMutationEntry" ,
113190 cause : Exception ,
114191 ):
115192 idempotent_msg = (
116193 "idempotent" if failed_mutation_entry .is_idempotent () else "non-idempotent"
117194 )
118- message = f"Failed { idempotent_msg } mutation entry at index { failed_idx } with cause: { cause !r} "
195+ index_msg = f" at index { failed_idx } " if failed_idx is not None else " "
196+ message = (
197+ f"Failed { idempotent_msg } mutation entry{ index_msg } with cause: { cause !r} "
198+ )
119199 super ().__init__ (message )
120200 self .index = failed_idx
121201 self .entry = failed_mutation_entry
0 commit comments