agentstr 0.1.11__py3-none-any.whl → 0.1.13__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.
@@ -1,296 +1,36 @@
1
- import ast
1
+ """
2
+ Module implementing the MerchantTools Toolkit for Agno agents.
3
+ """
4
+
2
5
  import json
3
6
  import logging
4
- import re
5
- from typing import Any, Dict, List, Optional, Tuple, Union, cast
6
-
7
- from agentstr.nostr import (
8
- EventId,
9
- Keys,
10
- NostrClient,
11
- ProductData,
12
- ShippingCost,
13
- ShippingMethod,
14
- StallData,
15
- )
16
-
17
- try:
18
- from phi.tools import Toolkit
19
- except ImportError:
20
- raise ImportError(
21
- "`phidata` not installed. Please install using `pip install phidata`"
22
- )
23
-
24
- from pydantic import BaseModel, ConfigDict, Field, validate_call
25
-
26
-
27
- class Profile:
28
-
29
- logger = logging.getLogger("Profile")
30
- WEB_URL: str = "https://primal.net/p/"
31
-
32
- def __init__(self, name: str, about: str, picture: str, nsec: Optional[str] = None):
33
- """Initialize the profile.
34
-
35
- Args:
36
- name: Name for the merchant
37
- about: brief description about the merchant
38
- picture: url to a png file with a picture for the merchant
39
- nsec: optional private key to be used by this Merchant
40
- """
41
-
42
- # Set log handling for MerchantProfile
43
- if not Profile.logger.hasHandlers():
44
- console_handler = logging.StreamHandler()
45
- console_handler.setLevel(logging.INFO)
46
- formatter = logging.Formatter(
47
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
48
- )
49
- console_handler.setFormatter(formatter)
50
- Profile.logger.addHandler(console_handler)
51
-
52
- self.name = name
53
- self.about = about
54
- self.picture = picture
55
-
56
- if nsec:
57
- self.private_key = nsec
58
- keys = Keys.parse(self.private_key)
59
- self.public_key = keys.public_key().to_bech32()
60
- Profile.logger.info(
61
- f"Pre-defined private key reused for {self.name}: {self.private_key}"
62
- )
63
- Profile.logger.info(
64
- f"Pre-defined public key reused for {self.name}: {self.public_key}"
65
- )
66
- else:
67
- keys = Keys.generate()
68
- self.private_key = keys.secret_key().to_bech32()
69
- self.public_key = keys.public_key().to_bech32()
70
- Profile.logger.info(
71
- f"New private key created for {self.name}: {self.private_key}"
72
- )
73
- Profile.logger.info(
74
- f"New public key created for {self.name}: {self.public_key}"
75
- )
76
-
77
- self.url = str(self.WEB_URL) + str(self.public_key)
78
-
79
- def __str__(self) -> str:
80
- return (
81
- "Merchant Profile:\n"
82
- "Name = {}\n"
83
- "Description = {}\n"
84
- "Picture = {}\n"
85
- "URL = {}\n"
86
- "Private key = {}\n"
87
- "Public key = {}".format(
88
- self.name,
89
- self.about,
90
- self.picture,
91
- self.url,
92
- self.private_key,
93
- self.public_key,
94
- )
95
- )
96
-
97
- def to_dict(self) -> dict:
98
- return {
99
- "name": self.name,
100
- "description": self.about,
101
- "picture": self.picture,
102
- "public key": self.public_key,
103
- "private key": self.private_key,
104
- }
105
-
106
- def get_about(self) -> str:
107
- """
108
- Returns a description of the Merchant
109
-
110
- Returns:
111
- str: description of the Merchant
112
- """
113
- return self.about
114
-
115
- def get_name(self) -> str:
116
- """
117
- Returns the Merchant's name
118
-
119
- Returns:
120
- str: Merchant's name
121
- """
122
- return self.name
123
-
124
- def get_picture(self) -> str:
125
- """
126
- Returns the picture associated with the Merchant.
127
-
128
- Returns:
129
- str: URL to the picture associated with the Merchant
130
- """
131
- return self.picture
132
-
133
- def get_private_key(self) -> str:
134
- """
135
- Returns the private key.
136
-
137
- Returns:
138
- str: private key in bech32 format
139
- """
140
- return str(self.private_key)
141
-
142
- def get_public_key(self) -> str:
143
- """
144
- Returns the public key.
145
-
146
- Returns:
147
- str: public key in bech32 format
148
- """
149
- return str(self.public_key)
150
-
151
- def get_url(self) -> str:
152
- return str(self.url)
153
-
154
-
155
- class MerchantProduct(BaseModel):
156
- model_config = ConfigDict(arbitrary_types_allowed=True)
157
-
158
- id: str
159
- stall_id: str
160
- name: str
161
- description: str
162
- images: List[str]
163
- currency: str
164
- price: float
165
- quantity: int
166
- shipping: List[ShippingCost]
167
- categories: Optional[List[str]] = []
168
- specs: Optional[List[List[str]]] = []
169
-
170
- @classmethod
171
- def from_product_data(cls, product: ProductData) -> "MerchantProduct":
172
- return cls(
173
- id=product.id,
174
- stall_id=product.stall_id,
175
- name=product.name,
176
- description=product.description,
177
- images=product.images,
178
- currency=product.currency,
179
- price=product.price,
180
- quantity=product.quantity,
181
- shipping=product.shipping,
182
- categories=product.categories if product.categories is not None else [],
183
- specs=product.specs if product.specs is not None else [],
184
- )
185
-
186
- def to_product_data(self) -> ProductData:
187
- return ProductData(
188
- id=self.id,
189
- stall_id=self.stall_id,
190
- name=self.name,
191
- description=self.description,
192
- images=self.images,
193
- currency=self.currency,
194
- price=self.price,
195
- quantity=self.quantity,
196
- shipping=self.shipping,
197
- categories=self.categories,
198
- specs=self.specs,
199
- )
200
-
201
- def to_dict(self) -> dict:
202
- """
203
- Returns a dictionary representation of the MerchantProduct.
204
- ShippingCost class is not serializable, so we need to convert it to a dictionary.
205
-
206
- Returns:
207
- dict: dictionary representation of the MerchantProduct
208
- """
209
- shipping_dicts = []
210
- for shipping in self.shipping:
211
- shipping_dicts.append({"id": shipping.id, "cost": shipping.cost})
212
-
213
- return {
214
- "id": self.id,
215
- "stall_id": self.stall_id,
216
- "name": self.name,
217
- "description": self.description,
218
- "images": self.images,
219
- "currency": self.currency,
220
- "price": self.price,
221
- "quantity": self.quantity,
222
- "shipping": shipping_dicts,
223
- "categories": self.categories,
224
- "specs": self.specs,
225
- }
226
-
227
-
228
- class MerchantStall(BaseModel):
229
- model_config = ConfigDict(arbitrary_types_allowed=True)
230
-
231
- id: str
232
- name: str
233
- description: str
234
- currency: str
235
- shipping: List[ShippingMethod]
236
-
237
- @classmethod
238
- def from_stall_data(cls, stall: StallData) -> "MerchantStall":
239
- return cls(
240
- id=stall.id(),
241
- name=stall.name(),
242
- description=stall.description(),
243
- currency=stall.currency(),
244
- shipping=stall.shipping(),
245
- )
246
-
247
- def to_stall_data(self) -> StallData:
248
- return StallData(
249
- self.id,
250
- self.name,
251
- self.description,
252
- self.currency,
253
- self.shipping, # No conversion needed
254
- )
7
+ import time
8
+ from typing import Any, List, Optional, Tuple, Union
255
9
 
256
- def to_dict(self) -> dict:
257
- """
258
- Returns a dictionary representation of the MerchantStall.
259
- ShippingMethod class is not serializable, so we need to convert it to a dictionary.
260
- We can only access cost and id from the ShippingMethod class. We can't access name or regions.
10
+ from pydantic import ConfigDict
261
11
 
262
- Returns:
263
- dict: dictionary representation of the MerchantStall
264
- """
265
- shipping_dicts = []
266
- for shipping in self.shipping:
267
- shipping_dicts.append(
268
- {
269
- "cost": shipping.get_shipping_cost().cost,
270
- "id": shipping.get_shipping_cost().id,
271
- }
272
- )
12
+ from agentstr.models import AgentProfile, MerchantProduct, MerchantStall
13
+ from agentstr.nostr import EventId, NostrClient
273
14
 
274
- return {
275
- "id": self.id,
276
- "name": self.name,
277
- "description": self.description,
278
- "currency": self.currency,
279
- "shipping zones": [shipping_dicts],
280
- }
15
+ try:
16
+ from agno.tools import Toolkit
17
+ except ImportError as exc:
18
+ raise ImportError(
19
+ "`agno` not installed. Please install using `pip install agno`"
20
+ ) from exc
281
21
 
282
22
 
283
- class Merchant(Toolkit):
23
+ class MerchantTools(Toolkit):
284
24
  """
285
- Merchant is a toolkit that allows a merchant to publish products and stalls to Nostr.
25
+ MerchantTools is a toolkit that allows a merchant to publish
26
+ products and stalls to Nostr.
286
27
 
287
28
  TBD:
288
- - Better differentiation between products and stalls in the database and products and stalls published.
29
+ - Better differentiation between products and stalls in the database
30
+ and products and stalls published.
289
31
 
290
32
  """
291
33
 
292
- from pydantic import ConfigDict
293
-
294
34
  model_config = ConfigDict(
295
35
  arbitrary_types_allowed=True, extra="allow", validate_assignment=True
296
36
  )
@@ -301,7 +41,7 @@ class Merchant(Toolkit):
301
41
 
302
42
  def __init__(
303
43
  self,
304
- merchant_profile: Profile,
44
+ merchant_profile: AgentProfile,
305
45
  relay: str,
306
46
  stalls: List[MerchantStall],
307
47
  products: List[MerchantProduct],
@@ -317,9 +57,7 @@ class Merchant(Toolkit):
317
57
  super().__init__(name="merchant")
318
58
  self.relay = relay
319
59
  self.merchant_profile = merchant_profile
320
- self._nostr_client = NostrClient(
321
- self.relay, self.merchant_profile.get_private_key()
322
- )
60
+ self._nostr_client = NostrClient(relay, merchant_profile.get_private_key())
323
61
 
324
62
  # initialize the Product DB with no event id
325
63
  self.product_db = [(product, None) for product in products]
@@ -327,7 +65,7 @@ class Merchant(Toolkit):
327
65
  # initialize the Stall DB with no event id
328
66
  self.stall_db = [(stall, None) for stall in stalls]
329
67
 
330
- # Register wrapped versions of the methods
68
+ # Register methods
331
69
  self.register(self.get_profile)
332
70
  self.register(self.get_relay)
333
71
  self.register(self.get_products)
@@ -345,21 +83,33 @@ class Merchant(Toolkit):
345
83
  self.register(self.remove_product_by_name)
346
84
  self.register(self.remove_stall_by_name)
347
85
 
86
+ def get_nostr_client(self) -> NostrClient:
87
+ """
88
+ Get the NostrClient instance
89
+ """
90
+ return self._nostr_client
91
+
348
92
  def get_profile(self) -> str:
349
93
  """
350
- Retrieves merchant profile in JSON format
94
+ Get the merchant profile in JSON format
351
95
 
352
96
  Returns:
353
97
  str: merchant profile in JSON format
354
98
  """
355
- return json.dumps(self.merchant_profile.to_dict())
99
+ return json.dumps(self.merchant_profile.to_json())
356
100
 
357
101
  def get_relay(self) -> str:
102
+ """
103
+ Get the Nostr relay the merchant is using
104
+
105
+ Returns:
106
+ str: Nostr relay
107
+ """
358
108
  return self.relay
359
109
 
360
110
  def get_products(self) -> str:
361
111
  """
362
- Retrieves all the merchant products
112
+ Get all the merchant products
363
113
 
364
114
  Returns:
365
115
  str: JSON string containing all products
@@ -368,7 +118,7 @@ class Merchant(Toolkit):
368
118
 
369
119
  def get_stalls(self) -> str:
370
120
  """
371
- Retrieves all the merchant stalls in JSON format
121
+ Get all the merchant stalls in JSON format
372
122
 
373
123
  Returns:
374
124
  str: JSON string containing all stalls
@@ -379,10 +129,13 @@ class Merchant(Toolkit):
379
129
  self,
380
130
  ) -> str:
381
131
  """
382
- Publishes or updates all products in the Merchant's Product DB
132
+ Publishes or updates to Nostrall products in the Merchant's Product DB
383
133
 
384
134
  Returns:
385
135
  str: JSON array with status of all product publishing operations
136
+
137
+ Raises:
138
+ ValueError: if NostrClient is not initialized
386
139
  """
387
140
 
388
141
  if self._nostr_client is None:
@@ -393,9 +146,9 @@ class Merchant(Toolkit):
393
146
  for i, (product, _) in enumerate(self.product_db):
394
147
  try:
395
148
  # Convert MerchantProduct to ProductData for nostr_client
396
- product_data = product.to_product_data()
149
+ # product_data = product.to_product_data()
397
150
  # Publish using the SDK's synchronous method
398
- event_id = self._nostr_client.publish_product(product_data)
151
+ event_id = self._nostr_client.publish_product(product)
399
152
  self.product_db[i] = (product, event_id)
400
153
  results.append(
401
154
  {
@@ -404,8 +157,10 @@ class Merchant(Toolkit):
404
157
  "product_name": product.name,
405
158
  }
406
159
  )
407
- except Exception as e:
408
- Profile.logger.error(f"Unable to publish product {product}. Error {e}")
160
+ # Pause for 0.5 seconds to avoid rate limiting
161
+ time.sleep(0.5)
162
+ except RuntimeError as e:
163
+ logging.error("Unable to publish product %s. Error %s", product, e)
409
164
  results.append(
410
165
  {"status": "error", "message": str(e), "product_name": product.name}
411
166
  )
@@ -416,10 +171,14 @@ class Merchant(Toolkit):
416
171
  self,
417
172
  ) -> str:
418
173
  """
419
- Publishes or updates all stalls managed by the merchant and adds the corresponding EventId to the Stall DB
174
+ Publishes or updates to Nostr all stalls managed by the merchant and adds
175
+ the corresponding EventId to the Stall DB
420
176
 
421
177
  Returns:
422
178
  str: JSON array with status of all stall publishing operations
179
+
180
+ Raises:
181
+ ValueError: if NostrClient is not initialized
423
182
  """
424
183
  if self._nostr_client is None:
425
184
  raise ValueError("NostrClient not initialized")
@@ -427,9 +186,9 @@ class Merchant(Toolkit):
427
186
 
428
187
  for i, (stall, _) in enumerate(self.stall_db):
429
188
  try:
430
- # Convert MerchantStall to StallData for nostr_client
431
- stall_data = stall.to_stall_data()
432
- event_id = self._nostr_client.publish_stall(stall_data)
189
+ # We don't need to convert MerchantStall to StallData for nostr_client
190
+ # stall_data = stall.to_stall_data()
191
+ event_id = self._nostr_client.publish_stall(stall)
433
192
  self.stall_db[i] = (stall, event_id)
434
193
  results.append(
435
194
  {
@@ -438,8 +197,10 @@ class Merchant(Toolkit):
438
197
  "stall_name": stall.name,
439
198
  }
440
199
  )
441
- except Exception as e:
442
- Profile.logger.error(f"Unable to publish stall {stall}. Error {e}")
200
+ # Pause for 0.5 seconds to avoid rate limiting
201
+ time.sleep(0.5)
202
+ except RuntimeError as e:
203
+ logging.error("Unable to publish stall %s. Error %s", stall, e)
443
204
  results.append(
444
205
  {"status": "error", "message": str(e), "stall_name": stall.name}
445
206
  )
@@ -448,13 +209,17 @@ class Merchant(Toolkit):
448
209
 
449
210
  def publish_new_product(self, product: MerchantProduct) -> str:
450
211
  """
451
- Publishes a new product that is not currently in the Merchant's Product DB and adds it to the Product DB
212
+ Publishes to Nostra new product that is not currently in the Merchant's
213
+ Product DB and adds it to the Product DB
452
214
 
453
215
  Args:
454
216
  product: MerchantProduct to be published
455
217
 
456
218
  Returns:
457
219
  str: JSON string with status of the operation
220
+
221
+ Raises:
222
+ ValueError: if NostrClient is not initialized
458
223
  """
459
224
  if self._nostr_client is None:
460
225
  raise ValueError("NostrClient not initialized")
@@ -473,19 +238,23 @@ class Merchant(Toolkit):
473
238
  "product_name": product.name,
474
239
  }
475
240
  )
476
- except Exception as e:
241
+ except RuntimeError as e:
477
242
  return json.dumps(
478
243
  {"status": "error", "message": str(e), "product_name": product.name}
479
244
  )
480
245
 
481
246
  def publish_product_by_name(self, arguments: str) -> str:
482
247
  """
483
- Publishes or updates a given product from the Merchant's Product DB
248
+ Publishes or updates to Nostra given product from the Merchant's Product DB
484
249
  Args:
485
- arguments: JSON string that may contain {"name": "product_name"} or just "product_name"
250
+ arguments: JSON string that may contain
251
+ {"name": "product_name"} or just "product_name"
486
252
 
487
253
  Returns:
488
254
  str: JSON string with status of the operation
255
+
256
+ Raises:
257
+ ValueError: if NostrClient is not initialized
489
258
  """
490
259
  if self._nostr_client is None:
491
260
  raise ValueError("NostrClient not initialized")
@@ -508,11 +277,13 @@ class Merchant(Toolkit):
508
277
  if product.name == name:
509
278
  try:
510
279
  # Convert MerchantProduct to ProductData for nostr_client
511
- product_data = product.to_product_data()
280
+ # product_data = product.to_product_data()
512
281
  # Publish using the SDK's synchronous method
513
- event_id = self._nostr_client.publish_product(product_data)
282
+ event_id = self._nostr_client.publish_product(product)
514
283
  # Update the product_db with the new event_id
515
284
  self.product_db[i] = (product, event_id)
285
+ # Pause for 0.5 seconds to avoid rate limiting
286
+ time.sleep(0.5)
516
287
  return json.dumps(
517
288
  {
518
289
  "status": "success",
@@ -520,7 +291,7 @@ class Merchant(Toolkit):
520
291
  "product_name": product.name,
521
292
  }
522
293
  )
523
- except Exception as e:
294
+ except RuntimeError as e:
524
295
  return json.dumps(
525
296
  {
526
297
  "status": "error",
@@ -540,7 +311,7 @@ class Merchant(Toolkit):
540
311
 
541
312
  def publish_products_by_stall_name(self, arguments: Union[str, dict]) -> str:
542
313
  """
543
- Publishes or updates all products sold by the merchant in a given stall
314
+ Publishes or updates to Nostr all products sold by the merchant in a given stall
544
315
 
545
316
  Args:
546
317
  arguments: str or dict with the stall name. Can be in formats:
@@ -550,6 +321,9 @@ class Merchant(Toolkit):
550
321
 
551
322
  Returns:
552
323
  str: JSON array with status of all product publishing operations
324
+
325
+ Raises:
326
+ ValueError: if NostrClient is not initialized
553
327
  """
554
328
  if self._nostr_client is None:
555
329
  raise ValueError("NostrClient not initialized")
@@ -604,8 +378,8 @@ class Merchant(Toolkit):
604
378
  for i, (product, _) in enumerate(self.product_db):
605
379
  if product.stall_id == stall_id:
606
380
  try:
607
- product_data = product.to_product_data()
608
- event_id = self._nostr_client.publish_product(product_data)
381
+ # product_data = product.to_product_data()
382
+ event_id = self._nostr_client.publish_product(product)
609
383
  self.product_db[i] = (product, event_id)
610
384
  results.append(
611
385
  {
@@ -615,7 +389,9 @@ class Merchant(Toolkit):
615
389
  "stall_name": stall_name,
616
390
  }
617
391
  )
618
- except Exception as e:
392
+ # Pause for 0.5 seconds to avoid rate limiting
393
+ time.sleep(0.5)
394
+ except RuntimeError as e:
619
395
  results.append(
620
396
  {
621
397
  "status": "error",
@@ -638,14 +414,14 @@ class Merchant(Toolkit):
638
414
 
639
415
  return json.dumps(results)
640
416
 
641
- except Exception as e:
417
+ except RuntimeError as e:
642
418
  return json.dumps(
643
419
  [{"status": "error", "message": str(e), "arguments": str(arguments)}]
644
420
  )
645
421
 
646
422
  def publish_profile(self) -> str:
647
423
  """
648
- Publishes the profile on Nostr
424
+ Publishes the profile to Nostr
649
425
 
650
426
  Returns:
651
427
  str: JSON of the event that published the profile
@@ -663,27 +439,32 @@ class Merchant(Toolkit):
663
439
  self.merchant_profile.get_picture(),
664
440
  )
665
441
  return json.dumps(event_id.__dict__)
666
- except Exception as e:
667
- raise RuntimeError(f"Unable to publish the profile: {e}")
442
+ except RuntimeError as e:
443
+ raise RuntimeError(f"Unable to publish the profile: {e}") from e
668
444
 
669
445
  def publish_new_stall(self, stall: MerchantStall) -> str:
670
446
  """
671
- Publishes a new stall that is not currently in the Merchant's Stall DB and adds it to the Stall DB
447
+ Publishes to Nostr a new stall that is not currently in the Merchant's
448
+ Stall DB and adds it to the Stall DB
672
449
 
673
450
  Args:
674
451
  stall: MerchantStall to be published
675
452
 
676
453
  Returns:
677
454
  str: JSON string with status of the operation
455
+
456
+ Raises:
457
+ ValueError: if NostrClient is not initialized
678
458
  """
679
459
  if self._nostr_client is None:
680
460
  raise ValueError("NostrClient not initialized")
681
461
 
682
462
  try:
683
- # Convert to StallData for SDK
684
- stall_data = stall.to_stall_data()
685
- # Publish using the SDK's synchronous method
686
- event_id = self._nostr_client.publish_stall(stall_data)
463
+ # We don't ned to convert to StallData.
464
+ # nostr_client.publish_stall() accepts a MerchantStall
465
+ # stall_data = stall.to_stall_data()
466
+ # Publish using the synchronous method
467
+ event_id = self._nostr_client.publish_stall(stall)
687
468
  # we need to add the stall event id to the stall db
688
469
  self.stall_db.append((stall, event_id))
689
470
  return json.dumps(
@@ -693,12 +474,27 @@ class Merchant(Toolkit):
693
474
  "stall_name": stall.name,
694
475
  }
695
476
  )
696
- except Exception as e:
477
+ except RuntimeError as e:
697
478
  return json.dumps(
698
479
  {"status": "error", "message": str(e), "stall_name": stall.name}
699
480
  )
700
481
 
701
482
  def publish_stall_by_name(self, arguments: Union[str, dict]) -> str:
483
+ """
484
+ Publishes or updates to Nostr a given stall by name
485
+
486
+ Args:
487
+ arguments: str or dict with the stall name. Can be in formats:
488
+ - {"name": "stall_name"}
489
+ - {"arguments": "{\"name\": \"stall_name\"}"}
490
+ - "stall_name"
491
+
492
+ Returns:
493
+ str: JSON array with status of the operation
494
+
495
+ Raises:
496
+ ValueError: if NostrClient is not initialized
497
+ """
702
498
  if self._nostr_client is None:
703
499
  raise ValueError("NostrClient not initialized")
704
500
 
@@ -735,9 +531,10 @@ class Merchant(Toolkit):
735
531
  for i, (stall, _) in enumerate(self.stall_db):
736
532
  if stall.name == stall_name:
737
533
  try:
738
- stall_data = stall.to_stall_data()
739
- event_id = self._nostr_client.publish_stall(stall_data)
534
+ event_id = self._nostr_client.publish_stall(stall)
740
535
  self.stall_db[i] = (stall, event_id)
536
+ # Pause for 0.5 seconds to avoid rate limiting
537
+ time.sleep(0.5)
741
538
  return json.dumps(
742
539
  {
743
540
  "status": "success",
@@ -745,7 +542,8 @@ class Merchant(Toolkit):
745
542
  "stall_name": stall.name,
746
543
  }
747
544
  )
748
- except Exception as e:
545
+
546
+ except RuntimeError as e:
749
547
  return json.dumps(
750
548
  [
751
549
  {
@@ -767,17 +565,20 @@ class Merchant(Toolkit):
767
565
  ]
768
566
  )
769
567
 
770
- except Exception as e:
568
+ except RuntimeError as e:
771
569
  return json.dumps(
772
570
  [{"status": "error", "message": str(e), "stall_name": "unknown"}]
773
571
  )
774
572
 
775
573
  def remove_all_products(self) -> str:
776
574
  """
777
- Removes all published products from Nostr
575
+ Removes from Nostr all products published by the merchant
778
576
 
779
577
  Returns:
780
578
  str: JSON array with status of all product removal operations
579
+
580
+ Raises:
581
+ ValueError: if NostrClient is not initialized
781
582
  """
782
583
  if self._nostr_client is None:
783
584
  raise ValueError("NostrClient not initialized")
@@ -789,7 +590,9 @@ class Merchant(Toolkit):
789
590
  results.append(
790
591
  {
791
592
  "status": "skipped",
792
- "message": f"Product '{product.name}' has not been published yet",
593
+ "message": (
594
+ f"Product '{product.name}' has not been published yet"
595
+ ),
793
596
  "product_name": product.name,
794
597
  }
795
598
  )
@@ -810,7 +613,9 @@ class Merchant(Toolkit):
810
613
  "event_id": str(event_id),
811
614
  }
812
615
  )
813
- except Exception as e:
616
+ # Pause for 0.5 seconds to avoid rate limiting
617
+ time.sleep(0.5)
618
+ except RuntimeError as e:
814
619
  results.append(
815
620
  {"status": "error", "message": str(e), "product_name": product.name}
816
621
  )
@@ -819,10 +624,14 @@ class Merchant(Toolkit):
819
624
 
820
625
  def remove_all_stalls(self) -> str:
821
626
  """
822
- Removes all stalls and their products from Nostr
627
+ Removes from Nostr all stalls from the merchant and their
628
+ corresponding products
823
629
 
824
630
  Returns:
825
631
  str: JSON array with status of all removal operations
632
+
633
+ Raises:
634
+ ValueError: if NostrClient is not initialized
826
635
  """
827
636
  if self._nostr_client is None:
828
637
  raise ValueError("NostrClient not initialized")
@@ -841,7 +650,7 @@ class Merchant(Toolkit):
841
650
  results.append(
842
651
  {
843
652
  "status": "skipped",
844
- "message": f"Product '{product.name}' has not been published yet",
653
+ "message": "Unpublished product",
845
654
  "product_name": product.name,
846
655
  "stall_name": stall_name,
847
656
  }
@@ -863,7 +672,7 @@ class Merchant(Toolkit):
863
672
  "event_id": str(event_id),
864
673
  }
865
674
  )
866
- except Exception as e:
675
+ except RuntimeError as e:
867
676
  results.append(
868
677
  {
869
678
  "status": "error",
@@ -879,7 +688,7 @@ class Merchant(Toolkit):
879
688
  results.append(
880
689
  {
881
690
  "status": "skipped",
882
- "message": f"Stall '{stall_name}' has not been published yet",
691
+ "message": (f"Stall '{stall_name}' has not been published yet"),
883
692
  "stall_name": stall_name,
884
693
  }
885
694
  )
@@ -897,7 +706,9 @@ class Merchant(Toolkit):
897
706
  "event_id": str(stall_event_id),
898
707
  }
899
708
  )
900
- except Exception as e:
709
+ # Pause for 0.5 seconds to avoid rate limiting
710
+ time.sleep(0.5)
711
+ except RuntimeError as e:
901
712
  results.append(
902
713
  {"status": "error", "message": str(e), "stall_name": stall_name}
903
714
  )
@@ -906,13 +717,17 @@ class Merchant(Toolkit):
906
717
 
907
718
  def remove_product_by_name(self, arguments: str) -> str:
908
719
  """
909
- Deletes a product with the given name from Nostr
720
+ Removes from Nostr a product with the given name
910
721
 
911
722
  Args:
912
- arguments: JSON string that may contain {"name": "product_name"} or just "product_name"
723
+ arguments: JSON string that may contain {"name": "product_name"}
724
+ or just "product_name"
913
725
 
914
726
  Returns:
915
727
  str: JSON string with status of the operation
728
+
729
+ Raises:
730
+ ValueError: if NostrClient is not initialized
916
731
  """
917
732
  if self._nostr_client is None:
918
733
  raise ValueError("NostrClient not initialized")
@@ -949,6 +764,8 @@ class Merchant(Toolkit):
949
764
  )
950
765
  # Remove the event_id, keeping the product in the database
951
766
  self.product_db[i] = (product, None)
767
+ # Pause for 0.5 seconds to avoid rate limiting
768
+ time.sleep(0.5)
952
769
  return json.dumps(
953
770
  {
954
771
  "status": "success",
@@ -957,7 +774,7 @@ class Merchant(Toolkit):
957
774
  "event_id": str(event_id),
958
775
  }
959
776
  )
960
- except Exception as e:
777
+ except RuntimeError as e:
961
778
  return json.dumps(
962
779
  {"status": "error", "message": str(e), "product_name": name}
963
780
  )
@@ -972,7 +789,8 @@ class Merchant(Toolkit):
972
789
  )
973
790
 
974
791
  def remove_stall_by_name(self, arguments: Union[str, dict]) -> str:
975
- """Remove a stall and its products by name
792
+ """
793
+ Remove from Nostr a stall and its products by name
976
794
 
977
795
  Args:
978
796
  arguments: str or dict with the stall name. Can be in formats:
@@ -982,6 +800,9 @@ class Merchant(Toolkit):
982
800
 
983
801
  Returns:
984
802
  str: JSON array with status of the operation
803
+
804
+ Raises:
805
+ ValueError: if NostrClient is not initialized
985
806
  """
986
807
  if self._nostr_client is None:
987
808
  raise ValueError("NostrClient not initialized")
@@ -1042,7 +863,7 @@ class Merchant(Toolkit):
1042
863
  results.append(
1043
864
  {
1044
865
  "status": "skipped",
1045
- "message": f"Product '{product.name}' has not been published yet",
866
+ "message": "Unpublished product",
1046
867
  "product_name": product.name,
1047
868
  "stall_name": stall_name,
1048
869
  }
@@ -1063,7 +884,9 @@ class Merchant(Toolkit):
1063
884
  "event_id": str(event_id),
1064
885
  }
1065
886
  )
1066
- except Exception as e:
887
+ # Pause for 0.5 seconds to avoid rate limiting
888
+ time.sleep(0.5)
889
+ except RuntimeError as e:
1067
890
  results.append(
1068
891
  {
1069
892
  "status": "error",
@@ -1080,7 +903,9 @@ class Merchant(Toolkit):
1080
903
  results.append(
1081
904
  {
1082
905
  "status": "skipped",
1083
- "message": f"Stall '{stall_name}' has not been published yet",
906
+ "message": (
907
+ f"Stall '{stall_name}' has not been published yet"
908
+ ),
1084
909
  "stall_name": stall_name,
1085
910
  }
1086
911
  )
@@ -1101,7 +926,7 @@ class Merchant(Toolkit):
1101
926
  "event_id": str(stall_event_id),
1102
927
  }
1103
928
  )
1104
- except Exception as e:
929
+ except RuntimeError as e:
1105
930
  results.append(
1106
931
  {
1107
932
  "status": "error",
@@ -1112,20 +937,7 @@ class Merchant(Toolkit):
1112
937
 
1113
938
  return json.dumps(results)
1114
939
 
1115
- except Exception as e:
940
+ except RuntimeError as e:
1116
941
  return json.dumps(
1117
942
  [{"status": "error", "message": str(e), "stall_name": "unknown"}]
1118
943
  )
1119
-
1120
- def get_event_id(self, response: Any) -> str:
1121
- """Convert any response to a string event ID.
1122
-
1123
- Args:
1124
- response: Response that might contain an event ID
1125
-
1126
- Returns:
1127
- str: String representation of event ID or empty string if None
1128
- """
1129
- if response is None:
1130
- return ""
1131
- return str(response)