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/community.py ADDED
@@ -0,0 +1,854 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ from datetime import date, datetime, time
6
+
7
+ from prettytable import PrettyTable
8
+
9
+ from nectar.instance import shared_blockchain_instance
10
+
11
+ from .blockchainobject import BlockchainObject
12
+ from .exceptions import AccountDoesNotExistsException, OfflineHasNoRPCException
13
+ from .utils import (
14
+ addTzInfo,
15
+ formatTimeString,
16
+ )
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class Community(BlockchainObject):
22
+ """A class representing a Hive community with methods to interact with it.
23
+
24
+ This class provides an interface to access and manipulate community data on the Hive blockchain.
25
+ It extends BlockchainObject and provides additional community-specific functionality.
26
+
27
+ Args:
28
+ community: Either a community name (str) or a dictionary containing community data
29
+ observer: Observer account for personalized results (default: "")
30
+ full: If True, fetch full community data (default: True)
31
+ lazy: If True, use lazy loading (default: False)
32
+ blockchain_instance: Blockchain instance for RPC access
33
+
34
+ Attributes:
35
+ type_id (int): Type identifier for blockchain objects (2 for communities)
36
+
37
+ Example:
38
+ >>> from nectar.community import Community
39
+ >>> from nectar import Hive
40
+ >>> from nectar.nodelist import NodeList
41
+ >>> nodelist = NodeList()
42
+ >>> nodelist.update_nodes()
43
+ >>> hv = Hive(node=nodelist.get_hive_nodes())
44
+ >>> community = Community("hive-139531", blockchain_instance=hv)
45
+ >>> print(community)
46
+ <Community hive-139531>
47
+
48
+ Note:
49
+ This class includes caching to reduce API server load. Use refresh() to update
50
+ the data and clear_cache() to clear the cache.
51
+ """
52
+
53
+ type_id = 2
54
+
55
+ def __init__(
56
+ self,
57
+ community: str | dict,
58
+ observer: str = "",
59
+ full: bool = True,
60
+ lazy: bool = False,
61
+ blockchain_instance=None,
62
+ ) -> None:
63
+ """
64
+ Create a Community wrapper for the given community identifier or raw data.
65
+
66
+ If `community` is a dict, it will be normalized via _parse_json_data before initialization.
67
+ This sets instance flags (full, lazy, observer) and resolves the blockchain instance used
68
+ for RPC calls (falls back to the shared global instance). The object is constructed with
69
+ its identifier field set to "name".
70
+
71
+ Parameters:
72
+ community: Community name (str) or a dict with community data.
73
+ observer: Account name used to request personalized data (optional).
74
+ full: If True, load complete community data when available.
75
+ lazy: If True, defer loading detail until accessed.
76
+ """
77
+ self.full = full
78
+ self.lazy = lazy
79
+ self.observer = observer
80
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
81
+ if isinstance(community, dict):
82
+ community = self._parse_json_data(community)
83
+ super().__init__(
84
+ community, lazy=lazy, full=full, id_item="name", blockchain_instance=self.blockchain
85
+ )
86
+
87
+ def refresh(self) -> None:
88
+ """
89
+ Refresh the community's data from the blockchain.
90
+
91
+ Fetches the latest community record for this community's name via the bridge RPC and
92
+ reinitializes the Community object with the returned data (updating identifier and all fields).
93
+ If the instance is offline, the method returns without performing any RPC call.
94
+
95
+ Raises:
96
+ AccountDoesNotExistsException: If no community data is returned for this community name.
97
+ """
98
+ if not self.blockchain.is_connected():
99
+ return
100
+ self.blockchain.rpc.set_next_node_on_empty_reply(True)
101
+ community = self.blockchain.rpc.get_community(
102
+ {"name": self.identifier, "observer": self.observer}
103
+ )
104
+
105
+ if not community:
106
+ raise AccountDoesNotExistsException(self.identifier)
107
+ community = self._parse_json_data(community)
108
+ self.identifier = community["name"]
109
+ # self.blockchain.refresh_data()
110
+
111
+ super().__init__(
112
+ community,
113
+ id_item="name",
114
+ lazy=self.lazy,
115
+ full=self.full,
116
+ blockchain_instance=self.blockchain,
117
+ )
118
+
119
+ def _parse_json_data(self, community: dict) -> dict:
120
+ """Parse and convert community JSON data into proper Python types.
121
+
122
+ This internal method converts string representations of numbers to integers
123
+ and parses date strings into datetime objects with timezone information.
124
+
125
+ Args:
126
+ community: Dictionary containing raw community data from the API
127
+
128
+ Returns:
129
+ dict: Processed community data with proper Python types
130
+ """
131
+ # Convert string numbers to integers
132
+ int_fields = [
133
+ "sum_pending",
134
+ "subscribers",
135
+ "num_pending",
136
+ "num_authors",
137
+ ]
138
+ for field in int_fields:
139
+ if field in community and isinstance(community.get(field), str):
140
+ community[field] = int(community.get(field, 0))
141
+
142
+ # Parse date strings into datetime objects
143
+ date_fields = ["created_at"]
144
+ for field in date_fields:
145
+ if field in community and isinstance(community.get(field), str):
146
+ community[field] = addTzInfo(
147
+ datetime.strptime(
148
+ community.get(field, "1970-01-01 00:00:00"), "%Y-%m-%d %H:%M:%S"
149
+ )
150
+ )
151
+
152
+ return community
153
+
154
+ def json(self) -> dict:
155
+ """Convert the community data to a JSON-serializable dictionary.
156
+
157
+ This method prepares the community data for JSON serialization by converting
158
+ non-JSON-serializable types (like datetime objects) to strings.
159
+
160
+ Returns:
161
+ dict: A dictionary containing the community data in a JSON-serializable format
162
+ """
163
+ output = self.copy()
164
+
165
+ # Convert integer fields to strings for JSON serialization
166
+ int_fields = [
167
+ "sum_pending",
168
+ "subscribers",
169
+ "num_pending",
170
+ "num_authors",
171
+ ]
172
+
173
+ # Fields that should only be converted if non-zero
174
+ int_non_zero_fields = []
175
+
176
+ # Convert regular integer fields
177
+ for field in int_fields:
178
+ if field in output and isinstance(output[field], int):
179
+ output[field] = str(output[field])
180
+
181
+ # Convert non-zero integer fields
182
+ for field in int_non_zero_fields:
183
+ if field in output and isinstance(output[field], int) and output[field] != 0:
184
+ output[field] = str(output[field])
185
+
186
+ # Convert datetime fields to ISO format strings
187
+ date_fields = ["created_at"]
188
+ for field in date_fields:
189
+ if field in output:
190
+ date_val = output.get(field, datetime(1970, 1, 1, 0, 0))
191
+ if isinstance(date_val, (datetime, date, time)):
192
+ output[field] = formatTimeString(date_val).replace("T", " ")
193
+ else:
194
+ output[field] = date_val
195
+ return json.loads(str(json.dumps(output)))
196
+
197
+ def get_community_roles(self, limit: int = 100, last: str | None = None) -> list:
198
+ """Lists community roles
199
+
200
+ Args:
201
+ limit: Maximum number of roles to return (default: 100)
202
+ last: Account name of the last role from previous page for pagination
203
+
204
+ Returns:
205
+ list: List of community roles
206
+
207
+ Raises:
208
+ OfflineHasNoRPCException: If not connected to the blockchain
209
+ """
210
+ community = self["name"]
211
+ if not self.blockchain.is_connected():
212
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
213
+
214
+ params = {"community": community, "limit": limit}
215
+ if last is not None:
216
+ params["last"] = last
217
+
218
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
219
+ return self.blockchain.rpc.list_community_roles(params)
220
+
221
+ def get_subscribers(self, limit: int = 100, last: str | None = None) -> list:
222
+ """Returns subscribers
223
+
224
+ Args:
225
+ limit: Maximum number of subscribers to return (default: 100)
226
+ last: Account name of the last subscriber from previous page for pagination
227
+
228
+ Returns:
229
+ list: List of subscribers
230
+
231
+ Raises:
232
+ OfflineHasNoRPCException: If not connected to the blockchain
233
+ """
234
+ community = self["name"]
235
+ if not self.blockchain.is_connected():
236
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
237
+
238
+ params = {"community": community, "limit": limit}
239
+ if last is not None:
240
+ params["last"] = last
241
+
242
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
243
+ return self.blockchain.rpc.list_subscribers(params)
244
+
245
+ def get_activities(self, limit: int = 100, last_id: str | None = None) -> list:
246
+ """Returns community activity
247
+
248
+ Args:
249
+ limit: Maximum number of activities to return (default: 100)
250
+ last_id: ID of the last activity from previous page for pagination
251
+
252
+ Returns:
253
+ list: List of community activities
254
+
255
+ Raises:
256
+ OfflineHasNoRPCException: If not connected to the blockchain
257
+ """
258
+ community = self["name"]
259
+ if not self.blockchain.is_connected():
260
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
261
+
262
+ params = {"account": community, "limit": limit}
263
+ if last_id is not None:
264
+ params["last_id"] = last_id
265
+
266
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
267
+ return self.blockchain.rpc.account_notifications(params)
268
+
269
+ def get_ranked_posts(
270
+ self,
271
+ observer: str | None = None,
272
+ limit: int = 100,
273
+ start_author: str | None = None,
274
+ start_permlink: str | None = None,
275
+ sort: str = "created",
276
+ ) -> list:
277
+ """Returns community posts
278
+
279
+ Args:
280
+ observer: Account name of the observer (optional)
281
+ limit: Maximum number of posts to return (default: 100)
282
+ start_author: Author of the post to start from for pagination (optional)
283
+ start_permlink: Permlink of the post to start from for pagination (optional)
284
+ sort: Sort order (default: "created")
285
+
286
+ Returns:
287
+ list: List of community posts
288
+
289
+ Raises:
290
+ OfflineHasNoRPCException: If not connected to the blockchain
291
+ """
292
+ community = self["name"]
293
+ if not self.blockchain.is_connected():
294
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
295
+
296
+ params = {"tag": community, "limit": limit, "sort": sort}
297
+
298
+ if observer is not None:
299
+ params["observer"] = observer
300
+ if start_author is not None:
301
+ params["start_author"] = start_author
302
+ if start_permlink is not None:
303
+ params["start_permlink"] = start_permlink
304
+
305
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
306
+ return self.blockchain.rpc.get_ranked_posts(params)
307
+
308
+ def set_role(self, account: str, role: str, mod_account: str) -> dict:
309
+ """Set role for a given account in the community.
310
+
311
+ Args:
312
+ account: Account name to set the role for
313
+ role: Role to assign (member, mod, admin, owner, or guest)
314
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
315
+
316
+ Returns:
317
+ dict: Transaction result
318
+
319
+ Raises:
320
+ OfflineHasNoRPCException: If not connected to the blockchain
321
+ ValueError: If role is not one of the allowed values
322
+ """
323
+ valid_roles = {"member", "mod", "admin", "owner", "guest"}
324
+ if role.lower() not in valid_roles:
325
+ raise ValueError(f"Invalid role. Must be one of: {', '.join(valid_roles)}")
326
+
327
+ community = self["name"]
328
+ if not self.blockchain.is_connected():
329
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
330
+
331
+ json_body = [
332
+ "setRole",
333
+ {
334
+ "community": community,
335
+ "account": account,
336
+ "role": role.lower(),
337
+ },
338
+ ]
339
+ return self.blockchain.custom_json(
340
+ "community", json_body, required_posting_auths=[mod_account]
341
+ )
342
+
343
+ def set_user_title(self, account: str, title: str, mod_account: str) -> dict:
344
+ """Set the title for a given account in the community.
345
+
346
+ Args:
347
+ account: Account name to set the title for
348
+ title: Title to assign to the account
349
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
350
+
351
+ Returns:
352
+ dict: Transaction result
353
+
354
+ Raises:
355
+ OfflineHasNoRPCException: If not connected to the blockchain
356
+ ValueError: If account or title is empty
357
+ """
358
+ if not account or not isinstance(account, str):
359
+ raise ValueError("Account must be a non-empty string")
360
+
361
+ if not title or not isinstance(title, str):
362
+ raise ValueError("Title must be a non-empty string")
363
+
364
+ community = self["name"]
365
+ if not self.blockchain.is_connected():
366
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
367
+
368
+ json_body = [
369
+ "setUserTitle",
370
+ {
371
+ "community": community,
372
+ "account": account,
373
+ "title": title.strip(),
374
+ },
375
+ ]
376
+ return self.blockchain.custom_json(
377
+ "community", json_body, required_posting_auths=[mod_account]
378
+ )
379
+
380
+ def mute_post(self, account: str, permlink: str, notes: str, mod_account: str) -> dict:
381
+ """Mutes a post in the community.
382
+
383
+ Args:
384
+ account: Author of the post to mute
385
+ permlink: Permlink of the post to mute
386
+ notes: Reason for muting the post
387
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
388
+
389
+ Returns:
390
+ dict: Transaction result
391
+
392
+ Raises:
393
+ OfflineHasNoRPCException: If not connected to the blockchain
394
+ ValueError: If any required parameter is invalid
395
+ """
396
+ if not account or not isinstance(account, str):
397
+ raise ValueError("Account must be a non-empty string")
398
+ if not permlink or not isinstance(permlink, str):
399
+ raise ValueError("Permlink must be a non-empty string")
400
+ if not isinstance(notes, str):
401
+ raise ValueError("Notes must be a string")
402
+ if not mod_account or not isinstance(mod_account, str):
403
+ raise ValueError("Moderator account must be a non-empty string")
404
+
405
+ community = self["name"]
406
+ if not self.blockchain.is_connected():
407
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
408
+
409
+ json_body = [
410
+ "mutePost",
411
+ {
412
+ "community": community,
413
+ "account": account,
414
+ "permlink": permlink,
415
+ "notes": notes.strip(),
416
+ },
417
+ ]
418
+ return self.blockchain.custom_json(
419
+ "community", json_body, required_posting_auths=[mod_account]
420
+ )
421
+
422
+ def unmute_post(self, account: str, permlink: str, notes: str, mod_account: str) -> dict:
423
+ """Unmute a previously muted post in the community.
424
+
425
+ Args:
426
+ account: Author of the post to unmute
427
+ permlink: Permlink of the post to unmute
428
+ notes: Reason for unmuting the post
429
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
430
+
431
+ Returns:
432
+ dict: Transaction result
433
+
434
+ Raises:
435
+ OfflineHasNoRPCException: If not connected to the blockchain
436
+ ValueError: If any required parameter is invalid
437
+ """
438
+ if not account or not isinstance(account, str):
439
+ raise ValueError("Account must be a non-empty string")
440
+ if not permlink or not isinstance(permlink, str):
441
+ raise ValueError("Permlink must be a non-empty string")
442
+ if not isinstance(notes, str):
443
+ raise ValueError("Notes must be a string")
444
+ if not mod_account or not isinstance(mod_account, str):
445
+ raise ValueError("Moderator account must be a non-empty string")
446
+
447
+ community = self["name"]
448
+ if not self.blockchain.is_connected():
449
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
450
+
451
+ json_body = [
452
+ "unmutePost",
453
+ {
454
+ "community": community,
455
+ "account": account,
456
+ "permlink": permlink,
457
+ "notes": notes.strip(),
458
+ },
459
+ ]
460
+ return self.blockchain.custom_json(
461
+ "community", json_body, required_posting_auths=[mod_account]
462
+ )
463
+
464
+ def update_props(
465
+ self,
466
+ title: str,
467
+ about: str,
468
+ is_nsfw: bool,
469
+ description: str,
470
+ flag_text: str,
471
+ admin_account: str,
472
+ ) -> dict:
473
+ """Update community properties.
474
+
475
+ Args:
476
+ title: New title for the community (must be non-empty)
477
+ about: Brief description of the community
478
+ is_nsfw: Whether the community contains NSFW content
479
+ description: Detailed description of the community
480
+ flag_text: Text shown when flagging content in this community
481
+ admin_account: Account name of the admin performing this action
482
+
483
+ Returns:
484
+ dict: Transaction result
485
+
486
+ Raises:
487
+ OfflineHasNoRPCException: If not connected to the blockchain
488
+ ValueError: If any required parameter is invalid
489
+ """
490
+ if not title or not isinstance(title, str):
491
+ raise ValueError("Title must be a non-empty string")
492
+ if not isinstance(about, str):
493
+ about = ""
494
+ if not isinstance(description, str):
495
+ description = ""
496
+ if not isinstance(flag_text, str):
497
+ flag_text = ""
498
+ if not admin_account or not isinstance(admin_account, str):
499
+ raise ValueError("Admin account must be a non-empty string")
500
+
501
+ community = self["name"]
502
+ if not self.blockchain.is_connected():
503
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
504
+
505
+ json_body = [
506
+ "updateProps",
507
+ {
508
+ "community": community,
509
+ "props": {
510
+ "title": title.strip(),
511
+ "about": about.strip(),
512
+ "is_nsfw": bool(is_nsfw),
513
+ "description": description.strip(),
514
+ "flag_text": flag_text.strip(),
515
+ },
516
+ },
517
+ ]
518
+ return self.blockchain.custom_json(
519
+ "community", json_body, required_posting_auths=[admin_account]
520
+ )
521
+
522
+ def subscribe(self, account: str) -> dict:
523
+ """Subscribe an account to this community.
524
+
525
+ The account that calls this method will be subscribed to the community.
526
+ The same account must be used to sign the transaction.
527
+
528
+ Args:
529
+ account: Account name that wants to subscribe to the community
530
+
531
+ Returns:
532
+ dict: Transaction result
533
+
534
+ Raises:
535
+ OfflineHasNoRPCException: If not connected to the blockchain
536
+ ValueError: If account is invalid
537
+ """
538
+ if not account or not isinstance(account, str):
539
+ raise ValueError("Account must be a non-empty string")
540
+
541
+ community = self["name"]
542
+ if not self.blockchain.is_connected():
543
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
544
+
545
+ json_body = [
546
+ "subscribe",
547
+ {
548
+ "community": community,
549
+ },
550
+ ]
551
+ return self.blockchain.custom_json("community", json_body, required_posting_auths=[account])
552
+
553
+ def pin_post(self, account: str, permlink: str, mod_account: str) -> dict:
554
+ """Pin a post to the top of the community feed.
555
+
556
+ This method allows community moderators to pin a specific post to the top of the
557
+ community's feed. The post will remain pinned until it is manually unpinned.
558
+
559
+ Args:
560
+ account: Author of the post to pin
561
+ permlink: Permlink of the post to pin
562
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
563
+
564
+ Returns:
565
+ dict: Transaction result
566
+
567
+ Raises:
568
+ OfflineHasNoRPCException: If not connected to the blockchain
569
+ ValueError: If any required parameter is invalid
570
+ """
571
+ if not account or not isinstance(account, str):
572
+ raise ValueError("Account must be a non-empty string")
573
+ if not permlink or not isinstance(permlink, str):
574
+ raise ValueError("Permlink must be a non-empty string")
575
+ if not mod_account or not isinstance(mod_account, str):
576
+ raise ValueError("Moderator account must be a non-empty string")
577
+
578
+ community = self["name"]
579
+ if not self.blockchain.is_connected():
580
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
581
+
582
+ json_body = [
583
+ "pinPost",
584
+ {
585
+ "community": community,
586
+ "account": account,
587
+ "permlink": permlink,
588
+ },
589
+ ]
590
+ return self.blockchain.custom_json(
591
+ "community", json_body, required_posting_auths=[mod_account]
592
+ )
593
+
594
+ def unsubscribe(self, account: str) -> dict:
595
+ """Unsubscribe an account from this community.
596
+
597
+ The account that calls this method will be unsubscribed from the community.
598
+ The same account must be used to sign the transaction.
599
+
600
+ Args:
601
+ account: Account name that wants to unsubscribe from the community
602
+
603
+ Returns:
604
+ dict: Transaction result
605
+
606
+ Raises:
607
+ OfflineHasNoRPCException: If not connected to the blockchain
608
+ ValueError: If account is invalid
609
+ """
610
+ if not account or not isinstance(account, str):
611
+ raise ValueError("Account must be a non-empty string")
612
+
613
+ community = self["name"]
614
+ if not self.blockchain.is_connected():
615
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
616
+
617
+ json_body = [
618
+ "unsubscribe",
619
+ {
620
+ "community": community,
621
+ },
622
+ ]
623
+ return self.blockchain.custom_json("community", json_body, required_posting_auths=[account])
624
+
625
+ def unpin_post(self, account: str, permlink: str, mod_account: str) -> dict:
626
+ """Remove a post from being pinned at the top of the community feed.
627
+
628
+ This method allows community moderators to unpin a previously pinned post.
629
+ After unpinning, the post will return to its normal position in the feed.
630
+
631
+ Args:
632
+ account: Author of the post to unpin
633
+ permlink: Permlink of the post to unpin
634
+ mod_account: Account name of the moderator performing this action (must be mod or higher)
635
+
636
+ Returns:
637
+ dict: Transaction result
638
+
639
+ Raises:
640
+ OfflineHasNoRPCException: If not connected to the blockchain
641
+ ValueError: If any required parameter is invalid
642
+ """
643
+ if not account or not isinstance(account, str):
644
+ raise ValueError("Account must be a non-empty string")
645
+ if not permlink or not isinstance(permlink, str):
646
+ raise ValueError("Permlink must be a non-empty string")
647
+ if not mod_account or not isinstance(mod_account, str):
648
+ raise ValueError("Moderator account must be a non-empty string")
649
+
650
+ community = self["name"]
651
+ if not self.blockchain.is_connected():
652
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
653
+
654
+ json_body = [
655
+ "unpinPost",
656
+ {
657
+ "community": community,
658
+ "account": account,
659
+ "permlink": permlink,
660
+ },
661
+ ]
662
+ return self.blockchain.custom_json(
663
+ "community", json_body, required_posting_auths=[mod_account]
664
+ )
665
+
666
+ def flag_post(self, account: str, permlink: str, notes: str, reporter: str) -> dict:
667
+ """Report a post to the community moderators for review.
668
+
669
+ This method allows community members to flag posts that may violate
670
+ community guidelines. The post will be added to the community's
671
+ review queue for moderators to evaluate.
672
+
673
+ Args:
674
+ account: Author of the post being reported
675
+ permlink: Permlink of the post being reported
676
+ notes: Explanation of why the post is being reported
677
+ reporter: Account name of the user reporting the post
678
+
679
+ Returns:
680
+ dict: Transaction result
681
+
682
+ Raises:
683
+ OfflineHasNoRPCException: If not connected to the blockchain
684
+ ValueError: If any required parameter is invalid
685
+ """
686
+ if not account or not isinstance(account, str):
687
+ raise ValueError("Account must be a non-empty string")
688
+ if not permlink or not isinstance(permlink, str):
689
+ raise ValueError("Permlink must be a non-empty string")
690
+ if not notes or not isinstance(notes, str):
691
+ raise ValueError("Notes must be a string")
692
+ if not reporter or not isinstance(reporter, str):
693
+ raise ValueError("Reporter account must be a non-empty string")
694
+
695
+ community = self["name"]
696
+ if not self.blockchain.is_connected():
697
+ raise OfflineHasNoRPCException("No RPC available in offline mode!")
698
+
699
+ json_body = [
700
+ "flagPost",
701
+ {
702
+ "community": community,
703
+ "account": account,
704
+ "permlink": permlink,
705
+ "notes": notes.strip(),
706
+ },
707
+ ]
708
+ return self.blockchain.custom_json(
709
+ "community", json_body, required_posting_auths=[reporter]
710
+ )
711
+
712
+
713
+ class CommunityObject(list):
714
+ """A list-like container for Community objects with additional utility methods."""
715
+
716
+ def printAsTable(self) -> None:
717
+ """Print a formatted table of communities with key metrics.
718
+
719
+ The table includes the following columns:
720
+ - Nr.: Sequential number
721
+ - Name: Community name
722
+ - Title: Community title
723
+ - lang: Language code
724
+ - subscribers: Number of subscribers
725
+ - sum_pending: Sum of pending payouts
726
+ - num_pending: Number of pending posts
727
+ - num_authors: Number of unique authors
728
+ """
729
+ t = PrettyTable(
730
+ [
731
+ "Nr.",
732
+ "Name",
733
+ "Title",
734
+ "lang",
735
+ "subscribers",
736
+ "sum_pending",
737
+ "num_pending",
738
+ "num_authors",
739
+ ]
740
+ )
741
+ t.align = "l"
742
+ count = 0
743
+ for community in self:
744
+ count += 1
745
+ t.add_row(
746
+ [
747
+ str(count),
748
+ community["name"],
749
+ community["title"],
750
+ community["lang"],
751
+ community["subscribers"],
752
+ community["sum_pending"],
753
+ community["num_pending"],
754
+ community["num_authors"],
755
+ ]
756
+ )
757
+ print(t)
758
+
759
+
760
+ class Communities(CommunityObject):
761
+ """A list of communities with additional querying capabilities.
762
+
763
+ This class extends CommunityObject to provide methods for fetching and
764
+ searching communities from the blockchain.
765
+
766
+ Args:
767
+ sort: Sort order for communities (default: "rank")
768
+ observer: Observer account for personalized results (optional)
769
+ last: Last community name for pagination (optional)
770
+ limit: Maximum number of communities to fetch (default: 100)
771
+ lazy: If True, use lazy loading (default: False)
772
+ full: If True, fetch full community data (default: True)
773
+ blockchain_instance: Blockchain instance to use for RPC access
774
+ """
775
+
776
+ def __init__(
777
+ self,
778
+ sort: str = "rank",
779
+ observer: str | None = None,
780
+ last: str | None = None,
781
+ limit: int = 100,
782
+ lazy: bool = False,
783
+ full: bool = True,
784
+ blockchain_instance=None,
785
+ ) -> None:
786
+ """
787
+ Initialize a Communities collection by querying the blockchain for community metadata.
788
+
789
+ Fetches up to `limit` communities from the resolved blockchain instance using paginated bridge RPC calls and constructs Community objects from the results.
790
+
791
+ Parameters:
792
+ sort (str): Sort order for results (e.g., "rank"). Defaults to "rank".
793
+ observer (str | None): Account used to personalize results; passed through to the RPC call.
794
+ last (str | None): Starting community name for pagination; used as the RPC `last` parameter.
795
+ limit (int): Maximum number of communities to fetch (clamped per-request to 100). Defaults to 100.
796
+ lazy (bool): If True, created Community objects will use lazy loading. Defaults to False.
797
+ full (bool): If True, created Community objects will request full data. Defaults to True.
798
+
799
+ Notes:
800
+ - If no blockchain instance is connected, initialization returns early and yields an empty collection.
801
+ - The constructor ensures at most `limit` Community objects are created.
802
+ """
803
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
804
+
805
+ if not self.blockchain.is_connected():
806
+ return
807
+
808
+ communities = []
809
+ community_cnt = 0
810
+ batch_limit = min(100, limit) # Ensure we don't exceed the limit
811
+
812
+ while community_cnt < limit:
813
+ self.blockchain.rpc.set_next_node_on_empty_reply(False)
814
+ batch = self.blockchain.rpc.list_communities(
815
+ {"sort": sort, "observer": observer, "last": last, "limit": batch_limit},
816
+ )
817
+ if not batch: # No more communities to fetch
818
+ break
819
+
820
+ communities.extend(batch)
821
+ community_cnt += len(batch)
822
+ last = communities[-1]["name"]
823
+
824
+ # Adjust batch size for the next iteration if needed
825
+ if community_cnt + batch_limit > limit:
826
+ batch_limit = limit - community_cnt
827
+
828
+ super().__init__(
829
+ [
830
+ Community(x, lazy=lazy, full=full, blockchain_instance=self.blockchain)
831
+ for x in communities[:limit] # Ensure we don't exceed the limit
832
+ ]
833
+ )
834
+
835
+ def search_title(self, title: str) -> CommunityObject:
836
+ """Search for communities with titles containing the given string.
837
+
838
+ The search is case-insensitive.
839
+
840
+ Args:
841
+ title: Text to search for in community titles
842
+
843
+ Returns:
844
+ CommunityObject: A new CommunityObject containing matching communities
845
+ """
846
+ if not title or not isinstance(title, str):
847
+ raise ValueError("Title must be a non-empty string")
848
+
849
+ ret = CommunityObject()
850
+ title_lower = title.lower()
851
+ for community in self:
852
+ if title_lower in community["title"].lower():
853
+ ret.append(community)
854
+ return ret