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.
- hive_nectar-0.2.9.dist-info/METADATA +194 -0
- hive_nectar-0.2.9.dist-info/RECORD +87 -0
- hive_nectar-0.2.9.dist-info/WHEEL +4 -0
- hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
- hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +37 -0
- nectar/account.py +5076 -0
- nectar/amount.py +553 -0
- nectar/asciichart.py +303 -0
- nectar/asset.py +122 -0
- nectar/block.py +574 -0
- nectar/blockchain.py +1242 -0
- nectar/blockchaininstance.py +2590 -0
- nectar/blockchainobject.py +263 -0
- nectar/cli.py +5937 -0
- nectar/comment.py +1552 -0
- nectar/community.py +854 -0
- nectar/constants.py +95 -0
- nectar/discussions.py +1437 -0
- nectar/exceptions.py +152 -0
- nectar/haf.py +381 -0
- nectar/hive.py +630 -0
- nectar/imageuploader.py +114 -0
- nectar/instance.py +113 -0
- nectar/market.py +876 -0
- nectar/memo.py +542 -0
- nectar/message.py +379 -0
- nectar/nodelist.py +309 -0
- nectar/price.py +603 -0
- nectar/profile.py +74 -0
- nectar/py.typed +0 -0
- nectar/rc.py +333 -0
- nectar/snapshot.py +1024 -0
- nectar/storage.py +62 -0
- nectar/transactionbuilder.py +659 -0
- nectar/utils.py +630 -0
- nectar/version.py +3 -0
- nectar/vote.py +722 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +728 -0
- nectarapi/__init__.py +12 -0
- nectarapi/exceptions.py +126 -0
- nectarapi/graphenerpc.py +596 -0
- nectarapi/node.py +194 -0
- nectarapi/noderpc.py +79 -0
- nectarapi/openapi.py +107 -0
- nectarapi/py.typed +0 -0
- nectarapi/rpcutils.py +98 -0
- nectarapi/version.py +3 -0
- nectarbase/__init__.py +15 -0
- nectarbase/ledgertransactions.py +106 -0
- nectarbase/memo.py +242 -0
- nectarbase/objects.py +521 -0
- nectarbase/objecttypes.py +21 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1357 -0
- nectarbase/py.typed +0 -0
- nectarbase/signedtransactions.py +89 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +3 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +1121 -0
- nectargraphenebase/aes.py +49 -0
- nectargraphenebase/base58.py +197 -0
- nectargraphenebase/bip32.py +575 -0
- nectargraphenebase/bip38.py +110 -0
- nectargraphenebase/chains.py +15 -0
- nectargraphenebase/dictionary.py +2 -0
- nectargraphenebase/ecdsasig.py +309 -0
- nectargraphenebase/objects.py +130 -0
- nectargraphenebase/objecttypes.py +8 -0
- nectargraphenebase/operationids.py +5 -0
- nectargraphenebase/operations.py +25 -0
- nectargraphenebase/prefix.py +13 -0
- nectargraphenebase/py.typed +0 -0
- nectargraphenebase/signedtransactions.py +221 -0
- nectargraphenebase/types.py +557 -0
- nectargraphenebase/unsignedtransactions.py +288 -0
- nectargraphenebase/version.py +3 -0
- nectarstorage/__init__.py +57 -0
- nectarstorage/base.py +317 -0
- nectarstorage/exceptions.py +15 -0
- nectarstorage/interfaces.py +244 -0
- nectarstorage/masterpassword.py +237 -0
- nectarstorage/py.typed +0 -0
- nectarstorage/ram.py +27 -0
- 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)
|