hive-nectar 0.2.9__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.
Files changed (87) hide show
  1. hive_nectar-0.2.9.dist-info/METADATA +194 -0
  2. hive_nectar-0.2.9.dist-info/RECORD +87 -0
  3. hive_nectar-0.2.9.dist-info/WHEEL +4 -0
  4. hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +37 -0
  7. nectar/account.py +5076 -0
  8. nectar/amount.py +553 -0
  9. nectar/asciichart.py +303 -0
  10. nectar/asset.py +122 -0
  11. nectar/block.py +574 -0
  12. nectar/blockchain.py +1242 -0
  13. nectar/blockchaininstance.py +2590 -0
  14. nectar/blockchainobject.py +263 -0
  15. nectar/cli.py +5937 -0
  16. nectar/comment.py +1552 -0
  17. nectar/community.py +854 -0
  18. nectar/constants.py +95 -0
  19. nectar/discussions.py +1437 -0
  20. nectar/exceptions.py +152 -0
  21. nectar/haf.py +381 -0
  22. nectar/hive.py +630 -0
  23. nectar/imageuploader.py +114 -0
  24. nectar/instance.py +113 -0
  25. nectar/market.py +876 -0
  26. nectar/memo.py +542 -0
  27. nectar/message.py +379 -0
  28. nectar/nodelist.py +309 -0
  29. nectar/price.py +603 -0
  30. nectar/profile.py +74 -0
  31. nectar/py.typed +0 -0
  32. nectar/rc.py +333 -0
  33. nectar/snapshot.py +1024 -0
  34. nectar/storage.py +62 -0
  35. nectar/transactionbuilder.py +659 -0
  36. nectar/utils.py +630 -0
  37. nectar/version.py +3 -0
  38. nectar/vote.py +722 -0
  39. nectar/wallet.py +472 -0
  40. nectar/witness.py +728 -0
  41. nectarapi/__init__.py +12 -0
  42. nectarapi/exceptions.py +126 -0
  43. nectarapi/graphenerpc.py +596 -0
  44. nectarapi/node.py +194 -0
  45. nectarapi/noderpc.py +79 -0
  46. nectarapi/openapi.py +107 -0
  47. nectarapi/py.typed +0 -0
  48. nectarapi/rpcutils.py +98 -0
  49. nectarapi/version.py +3 -0
  50. nectarbase/__init__.py +15 -0
  51. nectarbase/ledgertransactions.py +106 -0
  52. nectarbase/memo.py +242 -0
  53. nectarbase/objects.py +521 -0
  54. nectarbase/objecttypes.py +21 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1357 -0
  57. nectarbase/py.typed +0 -0
  58. nectarbase/signedtransactions.py +89 -0
  59. nectarbase/transactions.py +11 -0
  60. nectarbase/version.py +3 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +1121 -0
  63. nectargraphenebase/aes.py +49 -0
  64. nectargraphenebase/base58.py +197 -0
  65. nectargraphenebase/bip32.py +575 -0
  66. nectargraphenebase/bip38.py +110 -0
  67. nectargraphenebase/chains.py +15 -0
  68. nectargraphenebase/dictionary.py +2 -0
  69. nectargraphenebase/ecdsasig.py +309 -0
  70. nectargraphenebase/objects.py +130 -0
  71. nectargraphenebase/objecttypes.py +8 -0
  72. nectargraphenebase/operationids.py +5 -0
  73. nectargraphenebase/operations.py +25 -0
  74. nectargraphenebase/prefix.py +13 -0
  75. nectargraphenebase/py.typed +0 -0
  76. nectargraphenebase/signedtransactions.py +221 -0
  77. nectargraphenebase/types.py +557 -0
  78. nectargraphenebase/unsignedtransactions.py +288 -0
  79. nectargraphenebase/version.py +3 -0
  80. nectarstorage/__init__.py +57 -0
  81. nectarstorage/base.py +317 -0
  82. nectarstorage/exceptions.py +15 -0
  83. nectarstorage/interfaces.py +244 -0
  84. nectarstorage/masterpassword.py +237 -0
  85. nectarstorage/py.typed +0 -0
  86. nectarstorage/ram.py +27 -0
  87. nectarstorage/sqlite.py +343 -0
nectar/vote.py ADDED
@@ -0,0 +1,722 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import date, datetime, timezone
5
+
6
+ from prettytable import PrettyTable
7
+
8
+ from nectarapi.exceptions import InvalidParameters, UnknownKey
9
+
10
+ from .account import Account
11
+ from .amount import Amount
12
+ from .blockchainobject import BlockchainObject
13
+ from .comment import Comment
14
+ from .exceptions import VoteDoesNotExistsException
15
+ from .instance import shared_blockchain_instance
16
+ from .utils import (
17
+ addTzInfo,
18
+ construct_authorperm,
19
+ construct_authorpermvoter,
20
+ formatTimeString,
21
+ parse_time,
22
+ reputation_to_score,
23
+ resolve_authorperm,
24
+ resolve_authorpermvoter,
25
+ )
26
+
27
+
28
+ class Vote(BlockchainObject):
29
+ """Read data about a Vote in the chain
30
+
31
+ :param str authorperm: perm link to post/comment
32
+ :param nectar.nectar.nectar blockchain_instance: nectar
33
+ instance to use when accessing an RPC
34
+ """
35
+
36
+ def __init__(
37
+ self, voter, authorperm=None, lazy=False, full=False, blockchain_instance=None, **kwargs
38
+ ):
39
+ """
40
+ Initialize a Vote object representing a single vote on a post or comment.
41
+
42
+ Supports multiple input shapes for `voter`:
43
+ - voter as str with `authorperm` provided: `voter` is the voter name; `authorperm` is parsed into author/permlink.
44
+ - voter as dict containing "author", "permlink", and "voter": uses those fields directly.
45
+ - voter as dict with "authorperm" plus an external `authorperm` argument: resolves author/permlink and fills missing fields.
46
+ - voter as dict with "voter" plus an external `authorperm` argument: resolves author/permlink and fills missing fields.
47
+ - otherwise treats `voter` as an authorpermvoter token (author+permlink+voter), resolving author, permlink, and voter from it.
48
+
49
+ Behavior:
50
+ - Normalizes numeric/time fields via internal parsing before initializing the underlying BlockchainObject.
51
+ - Chooses the blockchain instance in this order: explicit `blockchain_instance`, then a shared default instance.
52
+ - Validates keyword arguments and raises on unknown or conflicting legacy instance keys.
53
+ """
54
+ # Check for unknown kwargs
55
+ if kwargs:
56
+ raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}")
57
+
58
+ self.full = full
59
+ self.lazy = lazy
60
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
61
+ if isinstance(voter, str) and authorperm is not None:
62
+ [author, permlink] = resolve_authorperm(authorperm)
63
+ self["voter"] = voter
64
+ self["author"] = author
65
+ self["permlink"] = permlink
66
+ authorpermvoter = construct_authorpermvoter(author, permlink, voter)
67
+ self["authorpermvoter"] = authorpermvoter
68
+ elif (
69
+ isinstance(voter, dict)
70
+ and "author" in voter
71
+ and "permlink" in voter
72
+ and "voter" in voter
73
+ ):
74
+ authorpermvoter = voter
75
+ authorpermvoter["authorpermvoter"] = construct_authorpermvoter(
76
+ voter["author"], voter["permlink"], voter["voter"]
77
+ )
78
+ authorpermvoter = self._parse_json_data(authorpermvoter)
79
+ elif isinstance(voter, dict) and "authorperm" in voter and authorperm is not None:
80
+ [author, permlink] = resolve_authorperm(voter["authorperm"])
81
+ authorpermvoter = voter
82
+ authorpermvoter["voter"] = authorperm
83
+ authorpermvoter["author"] = author
84
+ authorpermvoter["permlink"] = permlink
85
+ authorpermvoter["authorpermvoter"] = construct_authorpermvoter(
86
+ author, permlink, authorperm
87
+ )
88
+ authorpermvoter = self._parse_json_data(authorpermvoter)
89
+ elif isinstance(voter, dict) and "voter" in voter and authorperm is not None:
90
+ [author, permlink] = resolve_authorperm(authorperm)
91
+ authorpermvoter = voter
92
+ authorpermvoter["author"] = author
93
+ authorpermvoter["permlink"] = permlink
94
+ authorpermvoter["authorpermvoter"] = construct_authorpermvoter(
95
+ author, permlink, voter["voter"]
96
+ )
97
+ authorpermvoter = self._parse_json_data(authorpermvoter)
98
+ else:
99
+ authorpermvoter = voter
100
+ [author, permlink, voter] = resolve_authorpermvoter(authorpermvoter)
101
+ self["author"] = author
102
+ self["permlink"] = permlink
103
+
104
+ super().__init__(
105
+ authorpermvoter,
106
+ id_item="authorpermvoter",
107
+ lazy=lazy,
108
+ full=full,
109
+ blockchain_instance=self.blockchain,
110
+ )
111
+
112
+ def refresh(self):
113
+ """
114
+ Refresh the Vote object from the blockchain RPC, replacing its internal data with the latest on-chain vote.
115
+
116
+ If the object has no identifier or the blockchain is not connected, this method returns immediately. It resolves author, permlink, and voter from the stored identifier and queries the node for active votes. If the matching vote is found, the object is reinitialized with the normalized vote data; otherwise VoteDoesNotExistsException is raised.
117
+
118
+ Raises:
119
+ VoteDoesNotExistsException: if the vote cannot be found or the RPC indicates the vote does not exist.
120
+ """
121
+ if self.identifier is None:
122
+ return
123
+ if not self.blockchain.is_connected():
124
+ return
125
+ [author, permlink, voter] = resolve_authorpermvoter(str(self.identifier))
126
+ try:
127
+ self.blockchain.rpc.set_next_node_on_empty_reply(True)
128
+ try:
129
+ response = self.blockchain.rpc.get_active_votes(
130
+ author,
131
+ permlink,
132
+ )
133
+ votes = response["votes"] if isinstance(response, dict) else response
134
+ except InvalidParameters:
135
+ raise VoteDoesNotExistsException(self.identifier)
136
+ except Exception:
137
+ votes = self.blockchain.rpc.get_active_votes(
138
+ author,
139
+ permlink,
140
+ )
141
+ if isinstance(votes, dict) and "votes" in votes:
142
+ votes = votes["votes"]
143
+ except UnknownKey:
144
+ raise VoteDoesNotExistsException(self.identifier)
145
+
146
+ vote = None
147
+ if votes is not None:
148
+ for x in votes:
149
+ if x["voter"] == voter:
150
+ vote = x
151
+ if not vote:
152
+ raise VoteDoesNotExistsException(self.identifier)
153
+ vote = self._parse_json_data(vote)
154
+ vote["authorpermvoter"] = construct_authorpermvoter(author, permlink, voter)
155
+ super().__init__(
156
+ vote,
157
+ id_item="authorpermvoter",
158
+ lazy=self.lazy,
159
+ full=self.full,
160
+ blockchain_instance=self.blockchain,
161
+ )
162
+
163
+ def _parse_json_data(self, vote):
164
+ parse_int = [
165
+ "rshares",
166
+ "reputation",
167
+ ]
168
+ for p in parse_int:
169
+ if p in vote and isinstance(vote.get(p), str):
170
+ vote[p] = int(vote.get(p, "0"))
171
+
172
+ if "time" in vote and isinstance(vote.get("time"), str) and vote.get("time") != "":
173
+ vote["time"] = parse_time(vote.get("time", "1970-01-01T00:00:00"))
174
+ elif (
175
+ "timestamp" in vote
176
+ and isinstance(vote.get("timestamp"), str)
177
+ and vote.get("timestamp") != ""
178
+ ):
179
+ vote["time"] = parse_time(vote.get("timestamp", "1970-01-01T00:00:00"))
180
+ elif (
181
+ "last_update" in vote
182
+ and isinstance(vote.get("last_update"), str)
183
+ and vote.get("last_update") != ""
184
+ ):
185
+ vote["last_update"] = parse_time(vote.get("last_update", "1970-01-01T00:00:00"))
186
+ else:
187
+ vote["time"] = parse_time("1970-01-01T00:00:00")
188
+ return vote
189
+
190
+ def json(self):
191
+ output = self.copy()
192
+ if "author" in output:
193
+ output.pop("author")
194
+ if "permlink" in output:
195
+ output.pop("permlink")
196
+ parse_times = ["time"]
197
+ for p in parse_times:
198
+ if p in output:
199
+ p_date = output.get(p, datetime(1970, 1, 1, 0, 0))
200
+ if isinstance(p_date, (datetime, date)):
201
+ output[p] = formatTimeString(p_date)
202
+ else:
203
+ output[p] = p_date
204
+ parse_int = [
205
+ "rshares",
206
+ "reputation",
207
+ ]
208
+ for p in parse_int:
209
+ if p in output and isinstance(output[p], int):
210
+ output[p] = str(output[p])
211
+ return json.loads(str(json.dumps(output)))
212
+
213
+ @property
214
+ def voter(self):
215
+ return self["voter"]
216
+
217
+ @property
218
+ def authorperm(self):
219
+ if "authorperm" in self:
220
+ return self["authorperm"]
221
+ elif "authorpermvoter" in self:
222
+ [author, permlink, voter] = resolve_authorpermvoter(self["authorpermvoter"])
223
+ return construct_authorperm(author, permlink)
224
+ elif "author" in self and "permlink" in self:
225
+ return construct_authorperm(self["author"], self["permlink"])
226
+ else:
227
+ return ""
228
+
229
+ @property
230
+ def votee(self):
231
+ votee = ""
232
+ authorperm = self.get("authorperm", "")
233
+ authorpermvoter = self.get("authorpermvoter", "")
234
+ if authorperm != "":
235
+ votee = resolve_authorperm(authorperm)[0]
236
+ elif authorpermvoter != "":
237
+ votee = resolve_authorpermvoter(authorpermvoter)[0]
238
+ return votee
239
+
240
+ @property
241
+ def weight(self):
242
+ """
243
+ Return the raw vote weight stored for this vote.
244
+
245
+ The value is read directly from the underlying vote data (self["weight"]) and
246
+ represents the weight field provided by the blockchain (type may be int).
247
+ """
248
+ return self["weight"]
249
+
250
+ @property
251
+ def hbd(self):
252
+ """
253
+ Return the HBD value equivalent of this vote's rshares.
254
+
255
+ Uses the bound blockchain instance's rshares_to_hbd to convert the vote's integer `rshares` (defaults to 0).
256
+
257
+ Returns:
258
+ float: HBD amount corresponding to the vote's rshares.
259
+ """
260
+ return self.blockchain.rshares_to_hbd(int(self.get("rshares", 0)))
261
+
262
+ @property
263
+ def token_backed_dollar(self):
264
+ # Hive-only: always convert to HBD
265
+ """
266
+ Convert this vote's rshares to HBD (Hive-backed dollar).
267
+
268
+ Uses the associated blockchain instance's rshares_to_hbd conversion on the vote's "rshares" field (defaults to 0 if missing). This is Hive-specific and always returns HBD-equivalent value for the vote.
269
+ """
270
+ return self.blockchain.rshares_to_hbd(int(self.get("rshares", 0)))
271
+
272
+ @property
273
+ def rshares(self):
274
+ """
275
+ Return the vote's raw `rshares` as an integer.
276
+
277
+ Converts the stored `rshares` value (which may be a string or number) to an int and returns it.
278
+ If `rshares` is missing, returns 0.
279
+ """
280
+ return int(self.get("rshares", 0))
281
+
282
+ @property
283
+ def percent(self):
284
+ return self.get("percent", 0)
285
+
286
+ @property
287
+ def reputation(self):
288
+ return self.get("reputation", 0)
289
+
290
+ @property
291
+ def rep(self):
292
+ return reputation_to_score(int(self.reputation))
293
+
294
+ @property
295
+ def time(self):
296
+ return self["time"]
297
+
298
+
299
+ class VotesObject(list):
300
+ def get_sorted_list(self, sort_key="time", reverse=True):
301
+ sortedList = sorted(
302
+ self,
303
+ key=lambda x: (datetime.now(timezone.utc) - x.time).total_seconds(),
304
+ reverse=reverse,
305
+ )
306
+ return sortedList
307
+
308
+ def printAsTable(
309
+ self,
310
+ voter=None,
311
+ votee=None,
312
+ start=None,
313
+ stop=None,
314
+ start_percent=None,
315
+ stop_percent=None,
316
+ sort_key="time",
317
+ reverse=True,
318
+ allow_refresh=True,
319
+ return_str=False,
320
+ **kwargs,
321
+ ):
322
+ """
323
+ Render the votes collection as a formatted table, with optional filtering and sorting.
324
+
325
+ Detailed behavior:
326
+ - Filters votes by voter name, votee (author), time window (start/stop), and percent range (start_percent/stop_percent).
327
+ - Sorts votes using sort_key (default "time") and reverse order flag.
328
+ - Formats columns: Voter, Votee, HBD (token equivalent), Time (human-readable delta), Rshares, Percent, Weight.
329
+ - If return_str is True, returns the table string; otherwise prints it to stdout.
330
+
331
+ Parameters:
332
+ voter (str, optional): Only include votes by this voter name.
333
+ votee (str, optional): Only include votes targeting this votee (author).
334
+ start (datetime or str, optional): Inclusive lower bound for vote time; timezone info is added if missing.
335
+ stop (datetime or str, optional): Inclusive upper bound for vote time; timezone info is added if missing.
336
+ start_percent (int, optional): Inclusive lower bound for vote percent.
337
+ stop_percent (int, optional): Inclusive upper bound for vote percent.
338
+ sort_key (str, optional): Attribute name used to sort votes (default "time").
339
+ reverse (bool, optional): If True, sort in descending order (default True).
340
+ allow_refresh (bool, optional): If False, prevents refreshing votes during iteration by marking them as cached.
341
+ return_str (bool, optional): If True, return the rendered table as a string; otherwise print it.
342
+ **kwargs: Passed through to PrettyTable.get_string when rendering the table.
343
+
344
+ Returns:
345
+ str or None: The table string when return_str is True; otherwise None (table is printed).
346
+ """
347
+ table_header = ["Voter", "Votee", "HBD", "Time", "Rshares", "Percent", "Weight"]
348
+ t = PrettyTable(table_header)
349
+ t.align = "l"
350
+ start = addTzInfo(start)
351
+ stop = addTzInfo(stop)
352
+ for vote in self.get_sorted_list(sort_key=sort_key, reverse=reverse):
353
+ if not allow_refresh:
354
+ vote.cached = True
355
+
356
+ d_time = vote.time
357
+ if d_time != formatTimeString("1970-01-01T00:00:00"):
358
+ td = datetime.now(timezone.utc) - d_time
359
+ timestr = (
360
+ str(td.days)
361
+ + " days "
362
+ + str(td.seconds // 3600)
363
+ + ":"
364
+ + str((td.seconds // 60) % 60)
365
+ )
366
+ else:
367
+ start = None
368
+ stop = None
369
+ timestr = ""
370
+
371
+ percent = vote.get("percent", "")
372
+ if percent == "":
373
+ start_percent = None
374
+ stop_percent = None
375
+ if (
376
+ (start is None or d_time >= start)
377
+ and (stop is None or d_time <= stop)
378
+ and (start_percent is None or percent >= start_percent)
379
+ and (stop_percent is None or percent <= stop_percent)
380
+ and (voter is None or vote["voter"] == voter)
381
+ and (votee is None or vote.votee == votee)
382
+ ):
383
+ percent = vote.get("percent", "")
384
+ if percent == "":
385
+ percent = vote.get("vote_percent", "")
386
+ t.add_row(
387
+ [
388
+ vote["voter"],
389
+ vote.votee,
390
+ str(round(vote.token_backed_dollar, 2)).ljust(5) + "$",
391
+ timestr,
392
+ vote.get("rshares", ""),
393
+ str(percent),
394
+ str(vote["weight"]),
395
+ ]
396
+ )
397
+
398
+ if return_str:
399
+ return t.get_string(**kwargs)
400
+ else:
401
+ print(t.get_string(**kwargs))
402
+
403
+ def get_list(
404
+ self,
405
+ var="voter",
406
+ voter=None,
407
+ votee=None,
408
+ start=None,
409
+ stop=None,
410
+ start_percent=None,
411
+ stop_percent=None,
412
+ sort_key="time",
413
+ reverse=True,
414
+ ):
415
+ vote_list = []
416
+ start = addTzInfo(start)
417
+ stop = addTzInfo(stop)
418
+ for vote in self.get_sorted_list(sort_key=sort_key, reverse=reverse):
419
+ d_time = vote.time
420
+ if d_time != formatTimeString("1970-01-01T00:00:00"):
421
+ start = None
422
+ stop = None
423
+ percent = vote.get("percent", "")
424
+ if percent == "":
425
+ percent = vote.get("vote_percent", "")
426
+ if percent == "":
427
+ start_percent = None
428
+ stop_percent = None
429
+ if (
430
+ (start is None or d_time >= start)
431
+ and (stop is None or d_time <= stop)
432
+ and (start_percent is None or percent >= start_percent)
433
+ and (stop_percent is None or percent <= stop_percent)
434
+ and (voter is None or vote["voter"] == voter)
435
+ and (votee is None or vote.votee == votee)
436
+ ):
437
+ v = ""
438
+ if var == "voter":
439
+ v = vote["voter"]
440
+ elif var == "votee":
441
+ v = vote.votee
442
+ elif var == "sbd" or var == "hbd":
443
+ v = vote.token_backed_dollar
444
+ elif var == "time":
445
+ v = d_time
446
+ elif var == "rshares":
447
+ v = vote.get("rshares", 0)
448
+ elif var == "percent":
449
+ v = percent
450
+ elif var == "weight":
451
+ v = vote["weight"]
452
+ vote_list.append(v)
453
+ return vote_list
454
+
455
+ def print_stats(self, return_str=False, **kwargs):
456
+ # Using built-in timezone support
457
+ """
458
+ Print or return a summary table of vote statistics for this collection.
459
+
460
+ If return_str is True, the formatted table is returned as a string; otherwise it is printed.
461
+ Accepts the same filtering and formatting keyword arguments used by printAsTable (e.g., voter, votee, start, stop, start_percent, stop_percent, sort_key, reverse).
462
+ """
463
+ table_header = ["voter", "votee", "hbd", "time", "rshares", "percent", "weight"]
464
+ t = PrettyTable(table_header)
465
+ t.align = "l"
466
+
467
+ def __contains__(self, item: object, /) -> bool: # type: ignore[override]
468
+ if isinstance(item, Account):
469
+ name = item["name"]
470
+ authorperm = ""
471
+ elif isinstance(item, Comment):
472
+ authorperm = item.authorperm
473
+ name = ""
474
+ else:
475
+ name = item
476
+ authorperm = item
477
+
478
+ return (
479
+ any([name == x.voter for x in self])
480
+ or any([name == x.votee for x in self])
481
+ or any([authorperm == x.authorperm for x in self])
482
+ )
483
+
484
+ def __str__(self):
485
+ return self.printAsTable(return_str=True)
486
+
487
+ def __repr__(self):
488
+ return "<{} {}>".format(
489
+ self.__class__.__name__, str(getattr(self, "identifier", "unknown"))
490
+ )
491
+
492
+
493
+ class ActiveVotes(VotesObject):
494
+ """Obtain a list of votes for a post
495
+
496
+ :param str authorperm: authorperm link
497
+ :param Blockchain blockchain_instance: Blockchain instance to use when accessing RPC
498
+ """
499
+
500
+ def __init__(self, authorperm, lazy=False, full=False, blockchain_instance=None, **kwargs):
501
+ """
502
+ Initialize an ActiveVotes collection for a post's active votes.
503
+
504
+ Creates Vote objects for each active vote on the given post (author/permlink) and stores them in the list.
505
+ Accepts multiple input shapes for authorperm:
506
+ - Comment: extracts author/permlink and uses its `active_votes` via RPC.
507
+ - str: an authorperm string, resolved to author and permlink.
508
+ - list: treated as a pre-fetched list of vote dicts.
509
+ - dict: expects keys "active_votes" and "authorperm".
510
+
511
+ If no explicit blockchain instance is provided, a shared instance is used. If the blockchain is not connected or no votes are found, initialization returns without populating the collection.
512
+
513
+ Raises:
514
+ ValueError: if multiple legacy instance parameters are provided.
515
+ VoteDoesNotExistsException: when the RPC reports invalid parameters for the requested post (no such post).
516
+ """
517
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
518
+ votes = None
519
+ if not self.blockchain.is_connected():
520
+ return None
521
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
522
+
523
+ if isinstance(authorperm, Comment):
524
+ # if 'active_votes' in authorperm and len(authorperm["active_votes"]) > 0:
525
+ # votes = authorperm["active_votes"]
526
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
527
+ votes = self.blockchain.rpc.get_active_votes(
528
+ authorperm["author"],
529
+ authorperm["permlink"],
530
+ )
531
+ if isinstance(votes, dict) and "votes" in votes:
532
+ votes = votes["votes"]
533
+ authorperm = authorperm["authorperm"]
534
+ elif isinstance(authorperm, str):
535
+ [author, permlink] = resolve_authorperm(authorperm)
536
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
537
+ votes = self.blockchain.rpc.get_active_votes(author, permlink)
538
+ if isinstance(votes, dict) and "votes" in votes:
539
+ votes = votes["votes"]
540
+ elif isinstance(authorperm, list):
541
+ votes = authorperm
542
+ authorperm = None
543
+ elif isinstance(authorperm, dict):
544
+ votes = authorperm["active_votes"]
545
+ authorperm = authorperm["authorperm"]
546
+ if votes is None:
547
+ return
548
+ self.identifier = authorperm
549
+ super().__init__(
550
+ [
551
+ Vote(
552
+ x,
553
+ authorperm=authorperm,
554
+ lazy=lazy,
555
+ full=full,
556
+ blockchain_instance=self.blockchain,
557
+ )
558
+ for x in votes
559
+ ]
560
+ )
561
+
562
+ def get_downvote_pct_to_zero(self, account):
563
+ """
564
+ Calculate the vote percent (internal units; -10000 = -100%) required for the given
565
+ account to downvote this post's pending payout to zero USING THE PAYOUT FORMULA
566
+ shared as reference (reward fund + median price + effective vesting shares),
567
+ adjusted by the account's current downvoting power.
568
+
569
+ If the account's full 100% downvote (at current downvoting power) is insufficient,
570
+ returns -10000.
571
+ """
572
+ from .account import Account
573
+
574
+ account = Account(account, blockchain_instance=self.blockchain)
575
+
576
+ # Obtain pending payout for the target post
577
+ # self.identifier is the authorperm set in ActiveVotes initializer
578
+ authorperm = getattr(self, "identifier", "")
579
+ if not authorperm:
580
+ return 0.0
581
+ comment = Comment(authorperm, blockchain_instance=self.blockchain)
582
+ try:
583
+ pending_payout = float(
584
+ Amount(
585
+ comment.get("pending_payout_value", "0.000 HBD"),
586
+ blockchain_instance=self.blockchain,
587
+ )
588
+ )
589
+ except Exception:
590
+ pending_payout = 0.0
591
+ if pending_payout <= 0:
592
+ return 0.0
593
+
594
+ # Reward fund and price inputs
595
+ reward_fund = self.blockchain.get_reward_funds()
596
+ recent_claims = int(reward_fund["recent_claims"]) if reward_fund else 0
597
+ if recent_claims == 0:
598
+ return -10000.0
599
+ reward_balance = float(
600
+ Amount(reward_fund["reward_balance"], blockchain_instance=self.blockchain)
601
+ )
602
+ median_price = self.blockchain.get_median_price()
603
+ if median_price is None:
604
+ return -10000.0
605
+ # Convert 1 HIVE at median price to HBD
606
+ HBD_per_HIVE = float(
607
+ median_price
608
+ * Amount(1, self.blockchain.hive_symbol, blockchain_instance=self.blockchain)
609
+ )
610
+
611
+ # Account stake and power (effective vests + downvoting power)
612
+ effective_vests = float(account.get_effective_vesting_shares())
613
+ final_vests = effective_vests * 1e6 # micro-vests as in reference
614
+ get_dvp = getattr(account, "get_downvoting_power", None)
615
+ downvote_power_pct = get_dvp() if callable(get_dvp) else account.get_voting_power()
616
+
617
+ # Reference power model (vote_power=100, vote_weight=100) with divisor 50
618
+ power = (100 * 100 / 10000) / 50.0 # = 0.0002
619
+ rshares_full = power * final_vests / 10000.0
620
+ current_downvote_value_hbd = (rshares_full * reward_balance / recent_claims) * HBD_per_HIVE
621
+ # Scale by current downvoting power percentage
622
+ current_downvote_value_hbd *= downvote_power_pct / 100.0
623
+
624
+ if current_downvote_value_hbd <= 0:
625
+ return -10000.0
626
+
627
+ # Percent needed in UI terms, convert to internal units (-10000..0)
628
+ percent_needed = (pending_payout / current_downvote_value_hbd) * 100.0
629
+ internal = -percent_needed * 100.0
630
+ if internal < -10000.0:
631
+ return -10000.0
632
+ if internal > 0.0:
633
+ return 0.0
634
+ return internal
635
+
636
+
637
+ class AccountVotes(VotesObject):
638
+ """Obtain a list of votes for an account
639
+ Lists the last 100+ votes on the given account.
640
+
641
+ :param str account: Account name
642
+ :param Blockchain blockchain_instance: Blockchain instance to use when accessing RPC
643
+ """
644
+
645
+ def __init__(
646
+ self,
647
+ account,
648
+ start: datetime | str | None = None,
649
+ stop: datetime | str | None = None,
650
+ raw_data=False,
651
+ lazy=False,
652
+ full=False,
653
+ blockchain_instance=None,
654
+ ):
655
+ """
656
+ Initialize AccountVotes by loading votes for a given account within an optional time window.
657
+
658
+ Creates a collection of votes retrieved from the account's historical votes. Each entry is either a Vote object (default) or the raw vote dict when `raw_data` is True. Time filtering is applied using `start` and `stop` (inclusive). Empty or missing timestamps are treated as the Unix epoch.
659
+
660
+ Parameters:
661
+ account: Account or str
662
+ Account name or Account object whose votes to load.
663
+ start: datetime | str | None
664
+ Inclusive lower bound for vote time. Accepts a timezone-aware datetime or a time string; None disables the lower bound.
665
+ stop: datetime | str | None
666
+ Inclusive upper bound for vote time. Accepts a timezone-aware datetime or a time string; None disables the upper bound.
667
+ raw_data: bool
668
+ If True, return raw vote dictionaries instead of Vote objects.
669
+ lazy: bool
670
+ Passed to Vote when constructing Vote objects; controls lazy loading behavior.
671
+ full: bool
672
+ Passed to Vote when constructing Vote objects; controls whether to fully populate vote data.
673
+ """
674
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
675
+ # Convert start/stop to datetime objects for comparison
676
+ start_dt = None
677
+ stop_dt = None
678
+ if start is not None:
679
+ if isinstance(start, str):
680
+ start_dt = datetime.strptime(start, "%Y-%m-%dT%H:%M:%S").replace(
681
+ tzinfo=timezone.utc
682
+ )
683
+ else:
684
+ start_dt = start
685
+ if stop is not None:
686
+ if isinstance(stop, str):
687
+ stop_dt = datetime.strptime(stop, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
688
+ else:
689
+ stop_dt = stop
690
+ account = Account(account, blockchain_instance=self.blockchain)
691
+ votes = account.get_account_votes()
692
+ self.identifier = account["name"]
693
+ vote_list = []
694
+ if votes is None:
695
+ votes = []
696
+ for x in votes:
697
+ time = x.get("time", "")
698
+ if time == "":
699
+ time = x.get("last_update", "")
700
+ if time != "":
701
+ x["time"] = time
702
+ if time != "" and isinstance(time, str):
703
+ d_time = datetime.strptime(time, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
704
+ elif isinstance(time, datetime):
705
+ d_time = time
706
+ else:
707
+ d_time = datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
708
+ if (start_dt is None or d_time >= start_dt) and (stop_dt is None or d_time <= stop_dt):
709
+ if not raw_data:
710
+ vote_list.append(
711
+ Vote(
712
+ x,
713
+ authorperm=account["name"],
714
+ lazy=lazy,
715
+ full=full,
716
+ blockchain_instance=self.blockchain,
717
+ )
718
+ )
719
+ else:
720
+ vote_list.append(x)
721
+
722
+ super().__init__(vote_list)