meshtrade 1.22.0__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of meshtrade might be problematic. Click here for more details.

@@ -1,16 +1,21 @@
1
- """
2
- This module provides a factory function for creating Amount protobuf messages.
1
+ """Amount utility functions for Mesh API.
2
+
3
+ This module provides utility functions for working with Amount protobuf messages,
4
+ including creation, validation, comparison, and arithmetic operations.
3
5
  """
4
6
 
5
- from decimal import ROUND_DOWN, Decimal
7
+ from decimal import ROUND_DOWN
8
+ from decimal import Decimal as PyDecimal
6
9
 
7
10
  from .amount_pb2 import Amount
8
11
  from .decimal_built_in_conversions import built_in_to_decimal, decimal_to_built_in
12
+ from .decimal_pb2 import Decimal
9
13
  from .ledger import get_ledger_no_decimal_places
14
+ from .token import new_undefined_token
10
15
  from .token_pb2 import Token
11
16
 
12
17
 
13
- def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal = Decimal("0.00000001")) -> Amount:
18
+ def new_amount(value: PyDecimal, token: Token, precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001")) -> Amount:
14
19
  """Creates a new Amount, ensuring the value conforms to system-wide limits.
15
20
 
16
21
  This function is the safe constructor for creating Amount protobuf messages.
@@ -67,7 +72,7 @@ def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal =
67
72
  # Truncate the validated value to the number of decimal places specified by the
68
73
  # token's ledger. ROUND_DOWN ensures the value is never inflated.
69
74
  truncated_value = value_after_roundtrip.quantize(
70
- Decimal(10) ** -get_ledger_no_decimal_places(token.ledger),
75
+ PyDecimal(10) ** -get_ledger_no_decimal_places(token.ledger),
71
76
  rounding=ROUND_DOWN,
72
77
  )
73
78
 
@@ -76,3 +81,422 @@ def new_amount(value: Decimal, token: Token, precision_loss_tolerance: Decimal =
76
81
  token=token,
77
82
  value=built_in_to_decimal(truncated_value),
78
83
  )
84
+
85
+
86
+ def new_undefined_amount(value: PyDecimal) -> Amount:
87
+ """Create a new Amount with the specified value and an undefined token.
88
+
89
+ This is useful as a placeholder or when the token type is not yet known.
90
+ Since undefined tokens don't have a valid ledger, this bypasses ledger
91
+ validation and creates the amount directly with full precision.
92
+
93
+ Args:
94
+ value: The decimal value for the amount
95
+
96
+ Returns:
97
+ A new Amount with the specified value and an undefined token
98
+
99
+ Example:
100
+ >>> from decimal import Decimal as PyDecimal
101
+ >>> amount = new_undefined_amount(PyDecimal("100"))
102
+ >>> amount_is_undefined(amount)
103
+ True
104
+ """
105
+ # Undefined tokens don't have a valid ledger, so we create the Amount directly
106
+ # without going through new_amount() which requires ledger validation
107
+ return Amount(
108
+ value=built_in_to_decimal(value),
109
+ token=new_undefined_token(),
110
+ )
111
+
112
+
113
+ def amount_set_value(
114
+ amount: Amount,
115
+ value: PyDecimal,
116
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
117
+ ) -> Amount:
118
+ """Create a new Amount with the given value and the same token as the input amount.
119
+
120
+ Despite its name, this function does NOT modify the input - it creates and returns a NEW Amount.
121
+
122
+ Args:
123
+ amount: The amount whose token to use
124
+ value: The decimal value for the new amount
125
+ precision_loss_tolerance: The maximum acceptable difference after validation
126
+ round-trip. Defaults to a small tolerance for robustness.
127
+
128
+ Returns:
129
+ A new Amount with the specified value and the same token
130
+
131
+ Raises:
132
+ ValueError: If amount is None
133
+ AssertionError: If the value exceeds system precision limits
134
+
135
+ Example:
136
+ >>> from decimal import Decimal as PyDecimal
137
+ >>> original = new_undefined_amount(PyDecimal("100"))
138
+ >>> modified = amount_set_value(original, PyDecimal("200"))
139
+ >>> # original is unchanged, modified is a new Amount with value 200
140
+ """
141
+ if amount is None:
142
+ raise ValueError("amount cannot be None")
143
+
144
+ # Check if the token is undefined (doesn't have a valid ledger)
145
+ from .token import token_is_undefined
146
+
147
+ if token_is_undefined(amount.token):
148
+ # For undefined tokens, create Amount directly without ledger validation
149
+ return Amount(
150
+ value=built_in_to_decimal(value),
151
+ token=amount.token,
152
+ )
153
+
154
+ return new_amount(value, amount.token, precision_loss_tolerance)
155
+
156
+
157
+ def amount_is_undefined(amount: Amount | None) -> bool:
158
+ """Check whether this amount has an undefined token.
159
+
160
+ An amount is considered undefined if its associated token is undefined.
161
+
162
+ Args:
163
+ amount: Amount to check (can be None)
164
+
165
+ Returns:
166
+ True if the amount's token is undefined or if amount is None, False otherwise
167
+
168
+ None Safety:
169
+ Returns True if amount is None
170
+
171
+ Example:
172
+ >>> amount = new_undefined_amount(PyDecimal("100"))
173
+ >>> amount_is_undefined(amount)
174
+ True
175
+ """
176
+ if amount is None:
177
+ return True
178
+
179
+ from .token import token_is_undefined
180
+
181
+ return token_is_undefined(amount.token)
182
+
183
+
184
+ def amount_is_same_type_as(amount1: Amount | None, amount2: Amount | None) -> bool:
185
+ """Check if two amounts have the same token type (same currency/asset).
186
+
187
+ This is useful for validating that amounts can be compared or combined arithmetically.
188
+
189
+ Args:
190
+ amount1: First amount (can be None)
191
+ amount2: Second amount (can be None)
192
+
193
+ Returns:
194
+ True if both amounts have equal tokens, False otherwise
195
+
196
+ None Safety:
197
+ Returns False if either amount is None
198
+
199
+ Example:
200
+ >>> usd_amount1 = new_undefined_amount(PyDecimal("100"))
201
+ >>> usd_amount2 = new_undefined_amount(PyDecimal("200"))
202
+ >>> amount_is_same_type_as(usd_amount1, usd_amount2)
203
+ True
204
+ """
205
+ if amount1 is None or amount2 is None:
206
+ return False
207
+
208
+ from .token import token_is_equal_to
209
+
210
+ return token_is_equal_to(amount1.token, amount2.token)
211
+
212
+
213
+ def amount_is_equal_to(amount1: Amount | None, amount2: Amount | None) -> bool:
214
+ """Check if two amounts are equal in both value and token type.
215
+
216
+ Two amounts are considered equal if they have the same decimal value AND the same token.
217
+
218
+ Args:
219
+ amount1: First amount (can be None)
220
+ amount2: Second amount (can be None)
221
+
222
+ Returns:
223
+ True if both amounts have equal values and tokens (or both are None), False otherwise
224
+
225
+ None Safety:
226
+ Returns True if both are None, False if only one is None
227
+
228
+ Example:
229
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
230
+ >>> amount2 = new_undefined_amount(PyDecimal("100"))
231
+ >>> amount_is_equal_to(amount1, amount2)
232
+ True
233
+ """
234
+ if amount1 is None and amount2 is None:
235
+ return True
236
+ if amount1 is None or amount2 is None:
237
+ return False
238
+
239
+ from .decimal_operations import decimal_equal
240
+ from .token import token_is_equal_to
241
+
242
+ return token_is_equal_to(amount1.token, amount2.token) and decimal_equal(amount1.value, amount2.value)
243
+
244
+
245
+ def amount_is_negative(amount: Amount | None) -> bool:
246
+ """Check whether the amount's value is less than zero.
247
+
248
+ Args:
249
+ amount: Amount to check (can be None)
250
+
251
+ Returns:
252
+ True if the value is negative (< 0), False otherwise
253
+
254
+ None Safety:
255
+ Returns False if amount is None
256
+
257
+ Example:
258
+ >>> amount = new_undefined_amount(PyDecimal("-50"))
259
+ >>> amount_is_negative(amount)
260
+ True
261
+ """
262
+ if amount is None:
263
+ return False
264
+
265
+ from .decimal_operations import decimal_is_negative
266
+
267
+ return decimal_is_negative(amount.value)
268
+
269
+
270
+ def amount_is_zero(amount: Amount | None) -> bool:
271
+ """Check whether the amount's value is exactly zero.
272
+
273
+ Args:
274
+ amount: Amount to check (can be None)
275
+
276
+ Returns:
277
+ True if the value is zero, False otherwise
278
+
279
+ None Safety:
280
+ Returns False if amount is None
281
+
282
+ Example:
283
+ >>> amount = new_undefined_amount(PyDecimal("0"))
284
+ >>> amount_is_zero(amount)
285
+ True
286
+ """
287
+ if amount is None:
288
+ return False
289
+
290
+ from .decimal_operations import decimal_is_zero
291
+
292
+ return decimal_is_zero(amount.value)
293
+
294
+
295
+ def amount_contains_fractions(amount: Amount | None) -> bool:
296
+ """Check whether the amount's value has any fractional (decimal) component.
297
+
298
+ This is useful for determining if an amount can be represented as a whole number.
299
+
300
+ Args:
301
+ amount: Amount to check (can be None)
302
+
303
+ Returns:
304
+ True if the value has fractional/decimal places, False otherwise
305
+
306
+ None Safety:
307
+ Returns False if amount is None
308
+
309
+ Example:
310
+ >>> amount1 = new_undefined_amount(PyDecimal("100.50"))
311
+ >>> amount_contains_fractions(amount1)
312
+ True
313
+ >>> amount2 = new_undefined_amount(PyDecimal("100"))
314
+ >>> amount_contains_fractions(amount2)
315
+ False
316
+ """
317
+ if amount is None:
318
+ return False
319
+
320
+ # Convert protobuf Decimal to Python Decimal
321
+ value = PyDecimal(amount.value.value) if amount.value and amount.value.value else PyDecimal(0)
322
+
323
+ # Check if truncating to 0 decimal places changes the value
324
+ return value.quantize(PyDecimal("1"), rounding=ROUND_DOWN) != value
325
+
326
+
327
+ def amount_add(
328
+ amount1: Amount,
329
+ amount2: Amount,
330
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
331
+ ) -> Amount:
332
+ """Add two amounts and return a new amount with the result.
333
+
334
+ The amounts must have the same token type (currency/asset).
335
+
336
+ Args:
337
+ amount1: First amount
338
+ amount2: Second amount (must have same token as amount1)
339
+ precision_loss_tolerance: The maximum acceptable difference after validation
340
+ round-trip. Defaults to a small tolerance for robustness.
341
+
342
+ Returns:
343
+ A new Amount containing the sum (amount1 + amount2)
344
+
345
+ Raises:
346
+ ValueError: If either amount is None or if the amounts have different token types
347
+ AssertionError: If the result exceeds system precision limits
348
+
349
+ Example:
350
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
351
+ >>> amount2 = new_undefined_amount(PyDecimal("30"))
352
+ >>> result = amount_add(amount1, amount2)
353
+ >>> # result value is 130
354
+ """
355
+ if amount1 is None:
356
+ raise ValueError("amount1 cannot be None")
357
+ if amount2 is None:
358
+ raise ValueError("amount2 cannot be None")
359
+
360
+ from .decimal_operations import decimal_add
361
+ from .token import token_is_equal_to, token_pretty_string
362
+
363
+ if not token_is_equal_to(amount1.token, amount2.token):
364
+ raise ValueError(
365
+ f"cannot do arithmetic on amounts of different token denominations: "
366
+ f"{token_pretty_string(amount1.token)} vs. {token_pretty_string(amount2.token)}"
367
+ )
368
+
369
+ new_value_decimal = decimal_add(amount1.value, amount2.value)
370
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
371
+
372
+ return amount_set_value(amount1, new_value_py, precision_loss_tolerance)
373
+
374
+
375
+ def amount_sub(
376
+ amount1: Amount,
377
+ amount2: Amount,
378
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
379
+ ) -> Amount:
380
+ """Subtract amount2 from amount1 and return a new amount with the result.
381
+
382
+ The amounts must have the same token type (currency/asset).
383
+
384
+ Args:
385
+ amount1: First amount
386
+ amount2: Second amount to subtract (must have same token as amount1)
387
+ precision_loss_tolerance: The maximum acceptable difference after validation
388
+ round-trip. Defaults to a small tolerance for robustness.
389
+
390
+ Returns:
391
+ A new Amount containing the difference (amount1 - amount2)
392
+
393
+ Raises:
394
+ ValueError: If either amount is None or if the amounts have different token types
395
+ AssertionError: If the result exceeds system precision limits
396
+
397
+ Example:
398
+ >>> amount1 = new_undefined_amount(PyDecimal("100"))
399
+ >>> amount2 = new_undefined_amount(PyDecimal("30"))
400
+ >>> result = amount_sub(amount1, amount2)
401
+ >>> # result value is 70
402
+ """
403
+ if amount1 is None:
404
+ raise ValueError("amount1 cannot be None")
405
+ if amount2 is None:
406
+ raise ValueError("amount2 cannot be None")
407
+
408
+ from .decimal_operations import decimal_sub
409
+ from .token import token_is_equal_to, token_pretty_string
410
+
411
+ if not token_is_equal_to(amount1.token, amount2.token):
412
+ raise ValueError(
413
+ f"cannot do arithmetic on amounts of different token denominations: "
414
+ f"{token_pretty_string(amount1.token)} vs. {token_pretty_string(amount2.token)}"
415
+ )
416
+
417
+ new_value_decimal = decimal_sub(amount1.value, amount2.value)
418
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
419
+
420
+ return amount_set_value(amount1, new_value_py, precision_loss_tolerance)
421
+
422
+
423
+ def amount_decimal_mul(
424
+ amount: Amount,
425
+ multiplier: PyDecimal,
426
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
427
+ ) -> Amount:
428
+ """Multiply this amount by a decimal value and return a new amount with the result.
429
+
430
+ The token type is preserved.
431
+
432
+ Args:
433
+ amount: The amount to multiply
434
+ multiplier: The decimal multiplier
435
+ precision_loss_tolerance: The maximum acceptable difference after validation
436
+ round-trip. Defaults to a small tolerance for robustness.
437
+
438
+ Returns:
439
+ A new Amount containing the product (amount * multiplier)
440
+
441
+ Raises:
442
+ ValueError: If amount is None
443
+ AssertionError: If the result exceeds system precision limits
444
+
445
+ Example:
446
+ >>> amount = new_undefined_amount(PyDecimal("100"))
447
+ >>> result = amount_decimal_mul(amount, PyDecimal("2"))
448
+ >>> # result value is 200
449
+ """
450
+ if amount is None:
451
+ raise ValueError("amount cannot be None")
452
+
453
+ from .decimal_operations import decimal_mul
454
+
455
+ multiplier_decimal = Decimal(value=str(multiplier))
456
+ new_value_decimal = decimal_mul(amount.value, multiplier_decimal)
457
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
458
+
459
+ return amount_set_value(amount, new_value_py, precision_loss_tolerance)
460
+
461
+
462
+ def amount_decimal_div(
463
+ amount: Amount,
464
+ divisor: PyDecimal,
465
+ precision_loss_tolerance: PyDecimal = PyDecimal("0.00000001"),
466
+ ) -> Amount:
467
+ """Divide this amount by a decimal value and return a new amount with the result.
468
+
469
+ The token type is preserved.
470
+
471
+ Args:
472
+ amount: The amount to divide
473
+ divisor: The decimal divisor (must not be zero)
474
+ precision_loss_tolerance: The maximum acceptable difference after validation
475
+ round-trip. Defaults to a small tolerance for robustness.
476
+
477
+ Returns:
478
+ A new Amount containing the quotient (amount / divisor)
479
+
480
+ Raises:
481
+ ValueError: If amount is None
482
+ ZeroDivisionError: If divisor is zero
483
+ AssertionError: If the result exceeds system precision limits
484
+
485
+ Example:
486
+ >>> amount = new_undefined_amount(PyDecimal("100"))
487
+ >>> result = amount_decimal_div(amount, PyDecimal("4"))
488
+ >>> # result value is 25
489
+ """
490
+ if amount is None:
491
+ raise ValueError("amount cannot be None")
492
+
493
+ if divisor == 0:
494
+ raise ZeroDivisionError("cannot divide amount by zero")
495
+
496
+ from .decimal_operations import decimal_div
497
+
498
+ divisor_decimal = Decimal(value=str(divisor))
499
+ new_value_decimal = decimal_div(amount.value, divisor_decimal)
500
+ new_value_py = PyDecimal(new_value_decimal.value) if new_value_decimal.value else PyDecimal(0)
501
+
502
+ return amount_set_value(amount, new_value_py, precision_loss_tolerance)
@@ -18,12 +18,17 @@ def built_in_to_decimal(decimal_value: decimal.Decimal) -> Decimal:
18
18
  )
19
19
 
20
20
 
21
- def decimal_to_built_in(decimal_value: Decimal) -> decimal.Decimal:
21
+ def decimal_to_built_in(decimal_value: Decimal | None) -> decimal.Decimal:
22
22
  """
23
23
  Converts an instance of the financial Decimal protobuf type to an instance of the
24
24
  built-in decimal.Decimal type.
25
25
 
26
- :param decimal_value: The decimal_pb2.Decimal object to convert.
26
+ None Safety:
27
+ Returns Decimal("0") if decimal_value is None
28
+
29
+ :param decimal_value: The decimal_pb2.Decimal object to convert (can be None).
27
30
  :return: The converted decimal.Decimal object.
28
31
  """
29
- return decimal.Decimal(decimal_value.value if decimal_value.value != "" else "0")
32
+ if decimal_value is None or not decimal_value.value or decimal_value.value.strip() == "":
33
+ return decimal.Decimal("0")
34
+ return decimal.Decimal(decimal_value.value)