agentstr 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)