agentstr 0.1.7__py3-none-any.whl → 0.1.8__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.
agentstr/marketplace.py CHANGED
@@ -1,163 +1,1131 @@
1
+ import ast
2
+ import json
1
3
  import logging
2
- from typing import Optional
3
- from . import nostr
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
+ )
4
16
 
5
17
  try:
6
18
  from phi.tools import Toolkit
7
19
  except ImportError:
8
- raise ImportError("`phidata` not installed. Please install using `pip install phidata`")
20
+ raise ImportError(
21
+ "`phidata` not installed. Please install using `pip install phidata`"
22
+ )
9
23
 
10
- try:
11
- import asyncio
12
- except ImportError:
13
- raise ImportError("`asyncio` not installed. Please install using `pip install asyncio`")
24
+ from pydantic import BaseModel, ConfigDict, Field, validate_call
14
25
 
15
- class MerchantProfile():
16
26
 
17
- logger = logging.getLogger("MerchantProfile")
18
-
19
- def __init__(
20
- self,
21
- name: str,
22
- about: str,
23
- picture: str,
24
- nsec: Optional[str] = None
25
- ):
26
- """Initialize the Merchant profile.
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.
27
34
 
28
35
  Args:
29
36
  name: Name for the merchant
30
37
  about: brief description about the merchant
31
38
  picture: url to a png file with a picture for the merchant
32
- nsec: private key to be used by this Merchant
39
+ nsec: optional private key to be used by this Merchant
33
40
  """
34
41
 
35
42
  # Set log handling for MerchantProfile
36
- if not MerchantProfile.logger.hasHandlers():
43
+ if not Profile.logger.hasHandlers():
37
44
  console_handler = logging.StreamHandler()
38
45
  console_handler.setLevel(logging.INFO)
39
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
46
+ formatter = logging.Formatter(
47
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
48
+ )
40
49
  console_handler.setFormatter(formatter)
41
- MerchantProfile.logger.addHandler(console_handler)
42
-
50
+ Profile.logger.addHandler(console_handler)
51
+
43
52
  self.name = name
44
53
  self.about = about
45
54
  self.picture = picture
46
55
 
47
56
  if nsec:
48
57
  self.private_key = nsec
49
- keys = nostr.Keys.parse(self.private_key)
58
+ keys = Keys.parse(self.private_key)
50
59
  self.public_key = keys.public_key().to_bech32()
51
- MerchantProfile.logger.info(f"Pre-defined private key reused for {self.name}: {self.private_key}")
52
- MerchantProfile.logger.info(f"Pre-defined public key reused for {self.name}: {self.public_key}")
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
+ )
53
66
  else:
54
- keys = nostr.Keys.generate()
67
+ keys = Keys.generate()
55
68
  self.private_key = keys.secret_key().to_bech32()
56
69
  self.public_key = keys.public_key().to_bech32()
57
- MerchantProfile.logger.info(f"New private key created for {self.name}: {self.private_key}")
58
- MerchantProfile.logger.info(f"New public key created for {self.name}: {self.public_key}")
59
-
60
- def merchant_profile_to_str(self) -> str:
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:
61
80
  return (
62
- f"Merchant name: {self.name}. "
63
- f"Merchant description: {self.about}. "
64
- f"Merchant picture URL: {self.picture}. "
65
- f"Private key: {self.private_key}. "
66
- f"Public key: {self.public_key}."
67
- )
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
+ }
68
105
 
69
- def get_public_key(self) -> str:
70
- return self.public_key
71
-
72
- def get_private_key(self) -> str:
73
- return self.private_key
74
-
75
- def get_name(self) -> str:
76
- return self.name
77
-
78
106
  def get_about(self) -> str:
107
+ """
108
+ Returns a description of the Merchant
109
+
110
+ Returns:
111
+ str: description of the Merchant
112
+ """
79
113
  return self.about
80
-
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
+
81
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
+ """
82
131
  return self.picture
83
-
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
+ )
255
+
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.
261
+
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
+ )
273
+
274
+ return {
275
+ "id": self.id,
276
+ "name": self.name,
277
+ "description": self.description,
278
+ "currency": self.currency,
279
+ "shipping zones": [shipping_dicts],
280
+ }
84
281
 
85
282
 
86
283
  class Merchant(Toolkit):
284
+ """
285
+ Merchant is a toolkit that allows a merchant to publish products and stalls to Nostr.
286
+
287
+ TBD:
288
+ - Better differentiation between products and stalls in the database and products and stalls published.
289
+
290
+ """
291
+
292
+ from pydantic import ConfigDict
293
+
294
+ model_config = ConfigDict(
295
+ arbitrary_types_allowed=True, extra="allow", validate_assignment=True
296
+ )
297
+
298
+ _nostr_client: Optional[NostrClient] = None
299
+ product_db: List[Tuple[MerchantProduct, Optional[EventId]]] = []
300
+ stall_db: List[Tuple[MerchantStall, Optional[EventId]]] = []
87
301
 
88
- WEB_URL: str = "http://njump.me/"
89
-
90
302
  def __init__(
91
303
  self,
92
- merchant_profile: MerchantProfile,
304
+ merchant_profile: Profile,
93
305
  relay: str,
306
+ stalls: List[MerchantStall],
307
+ products: List[MerchantProduct],
94
308
  ):
95
309
  """Initialize the Merchant toolkit.
96
310
 
97
311
  Args:
98
312
  merchant_profile: profile of the merchant using this agent
99
313
  relay: Nostr relay to use for communications
314
+ stalls: list of stalls managed by this merchant
315
+ products: list of products sold by this merchant
100
316
  """
101
317
  super().__init__(name="merchant")
102
318
  self.relay = relay
103
319
  self.merchant_profile = merchant_profile
320
+ self._nostr_client = NostrClient(
321
+ self.relay, self.merchant_profile.get_private_key()
322
+ )
323
+
324
+ # initialize the Product DB with no event id
325
+ self.product_db = [(product, None) for product in products]
104
326
 
105
- # Register all methods
106
- self.register(self.publish_merchant_profile)
107
- self.register(self.get_merchant_url)
108
-
109
- def publish_merchant_profile(
110
- self
327
+ # initialize the Stall DB with no event id
328
+ self.stall_db = [(stall, None) for stall in stalls]
329
+
330
+ # Register wrapped versions of the methods
331
+ self.register(self.get_profile)
332
+ self.register(self.get_relay)
333
+ self.register(self.get_products)
334
+ self.register(self.get_stalls)
335
+ self.register(self.publish_all_products)
336
+ self.register(self.publish_all_stalls)
337
+ self.register(self.publish_new_product)
338
+ self.register(self.publish_product_by_name)
339
+ self.register(self.publish_products_by_stall_name)
340
+ self.register(self.publish_profile)
341
+ self.register(self.publish_new_stall)
342
+ self.register(self.publish_stall_by_name)
343
+ self.register(self.remove_all_products)
344
+ self.register(self.remove_all_stalls)
345
+ self.register(self.remove_product_by_name)
346
+ self.register(self.remove_stall_by_name)
347
+
348
+ def get_profile(self) -> str:
349
+ """
350
+ Retrieves merchant profile in JSON format
351
+
352
+ Returns:
353
+ str: merchant profile in JSON format
354
+ """
355
+ return json.dumps(self.merchant_profile.to_dict())
356
+
357
+ def get_relay(self) -> str:
358
+ return self.relay
359
+
360
+ def get_products(self) -> str:
361
+ """
362
+ Retrieves all the merchant products
363
+
364
+ Returns:
365
+ str: JSON string containing all products
366
+ """
367
+ return json.dumps([p.to_dict() for p, _ in self.product_db])
368
+
369
+ def get_stalls(self) -> str:
370
+ """
371
+ Retrieves all the merchant stalls in JSON format
372
+
373
+ Returns:
374
+ str: JSON string containing all stalls
375
+ """
376
+ return json.dumps([s.to_dict() for s, _ in self.stall_db])
377
+
378
+ def publish_all_products(
379
+ self,
111
380
  ) -> str:
112
381
  """
113
- Publishes the merchant profile on Nostr
382
+ Publishes or updates all products in the Merchant's Product DB
114
383
 
115
384
  Returns:
116
- str: with event id and other details if successful or "error" string if unsuccesful
385
+ str: JSON array with status of all product publishing operations
117
386
  """
118
- # Run the async pubilshing function synchronously
119
- return asyncio.run(self._async_publish_merchant_profile())
120
-
121
- async def _async_publish_merchant_profile(
122
- self
387
+
388
+ if self._nostr_client is None:
389
+ raise ValueError("NostrClient not initialized")
390
+
391
+ results = []
392
+
393
+ for i, (product, _) in enumerate(self.product_db):
394
+ try:
395
+ # Convert MerchantProduct to ProductData for nostr_client
396
+ product_data = product.to_product_data()
397
+ # Publish using the SDK's synchronous method
398
+ event_id = self._nostr_client.publish_product(product_data)
399
+ self.product_db[i] = (product, event_id)
400
+ results.append(
401
+ {
402
+ "status": "success",
403
+ "event_id": str(event_id),
404
+ "product_name": product.name,
405
+ }
406
+ )
407
+ except Exception as e:
408
+ Profile.logger.error(f"Unable to publish product {product}. Error {e}")
409
+ results.append(
410
+ {"status": "error", "message": str(e), "product_name": product.name}
411
+ )
412
+
413
+ return json.dumps(results)
414
+
415
+ def publish_all_stalls(
416
+ self,
123
417
  ) -> str:
124
418
  """
125
- Asynchronous method to publish the merchant profile on Nostr
419
+ Publishes or updates all stalls managed by the merchant and adds the corresponding EventId to the Stall DB
126
420
 
127
421
  Returns:
128
- str: with event id and other details if successful or "error" string if unsuccesful
422
+ str: JSON array with status of all stall publishing operations
129
423
  """
130
-
131
- nostr_client = nostr.NostrClient(self.relay, self.merchant_profile.get_private_key())
132
-
133
- # Connect to the relay
134
- outcome = await nostr_client.connect()
424
+ if self._nostr_client is None:
425
+ raise ValueError("NostrClient not initialized")
426
+ results = []
135
427
 
136
- # Check if the operation resulted in an error
137
- if outcome == nostr.NostrClient.ERROR:
138
- return nostr.NostrClient.ERROR
139
- else:
140
- eventid = await nostr_client.publish_profile(
428
+ for i, (stall, _) in enumerate(self.stall_db):
429
+ 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)
433
+ self.stall_db[i] = (stall, event_id)
434
+ results.append(
435
+ {
436
+ "status": "success",
437
+ "event_id": str(event_id),
438
+ "stall_name": stall.name,
439
+ }
440
+ )
441
+ except Exception as e:
442
+ Profile.logger.error(f"Unable to publish stall {stall}. Error {e}")
443
+ results.append(
444
+ {"status": "error", "message": str(e), "stall_name": stall.name}
445
+ )
446
+
447
+ return json.dumps(results)
448
+
449
+ def publish_new_product(self, product: MerchantProduct) -> str:
450
+ """
451
+ Publishes a new product that is not currently in the Merchant's Product DB and adds it to the Product DB
452
+
453
+ Args:
454
+ product: MerchantProduct to be published
455
+
456
+ Returns:
457
+ str: JSON string with status of the operation
458
+ """
459
+ if self._nostr_client is None:
460
+ raise ValueError("NostrClient not initialized")
461
+
462
+ try:
463
+ # Convert MerchantProduct to ProductData for nostr_client
464
+ product_data = product.to_product_data()
465
+ # Publish using the SDK's synchronous method
466
+ event_id = self._nostr_client.publish_product(product_data)
467
+ # we need to add the product event id to the product db
468
+ self.product_db.append((product, event_id))
469
+ return json.dumps(
470
+ {
471
+ "status": "success",
472
+ "event_id": str(event_id),
473
+ "product_name": product.name,
474
+ }
475
+ )
476
+ except Exception as e:
477
+ return json.dumps(
478
+ {"status": "error", "message": str(e), "product_name": product.name}
479
+ )
480
+
481
+ def publish_product_by_name(self, arguments: str) -> str:
482
+ """
483
+ Publishes or updates a given product from the Merchant's Product DB
484
+ Args:
485
+ arguments: JSON string that may contain {"name": "product_name"} or just "product_name"
486
+
487
+ Returns:
488
+ str: JSON string with status of the operation
489
+ """
490
+ if self._nostr_client is None:
491
+ raise ValueError("NostrClient not initialized")
492
+
493
+ try:
494
+ # Try to parse as JSON first
495
+ if isinstance(arguments, dict):
496
+ parsed = arguments
497
+ else:
498
+ parsed = json.loads(arguments)
499
+ name = parsed.get(
500
+ "name", parsed
501
+ ) # Get name if exists, otherwise use whole value
502
+ except json.JSONDecodeError:
503
+ # If not JSON, use the raw string
504
+ name = arguments
505
+
506
+ # iterate through all products searching for the right name
507
+ for i, (product, _) in enumerate(self.product_db):
508
+ if product.name == name:
509
+ try:
510
+ # Convert MerchantProduct to ProductData for nostr_client
511
+ product_data = product.to_product_data()
512
+ # Publish using the SDK's synchronous method
513
+ event_id = self._nostr_client.publish_product(product_data)
514
+ # Update the product_db with the new event_id
515
+ self.product_db[i] = (product, event_id)
516
+ return json.dumps(
517
+ {
518
+ "status": "success",
519
+ "event_id": str(event_id),
520
+ "product_name": product.name,
521
+ }
522
+ )
523
+ except Exception as e:
524
+ return json.dumps(
525
+ {
526
+ "status": "error",
527
+ "message": str(e),
528
+ "product_name": product.name,
529
+ }
530
+ )
531
+
532
+ # If we are here, then we didn't find a match
533
+ return json.dumps(
534
+ {
535
+ "status": "error",
536
+ "message": f"Product '{name}' not found in database",
537
+ "product_name": name,
538
+ }
539
+ )
540
+
541
+ def publish_products_by_stall_name(self, arguments: Union[str, dict]) -> str:
542
+ """
543
+ Publishes or updates all products sold by the merchant in a given stall
544
+
545
+ Args:
546
+ arguments: str or dict with the stall name. Can be in formats:
547
+ - {"name": "stall_name"}
548
+ - {"arguments": "{\"name\": \"stall_name\"}"}
549
+ - "stall_name"
550
+
551
+ Returns:
552
+ str: JSON array with status of all product publishing operations
553
+ """
554
+ if self._nostr_client is None:
555
+ raise ValueError("NostrClient not initialized")
556
+
557
+ try:
558
+ # Parse arguments to get stall_name
559
+ stall_name: str
560
+ if isinstance(arguments, str):
561
+ try:
562
+ parsed = json.loads(arguments)
563
+ if isinstance(parsed, dict):
564
+ raw_name: Optional[Any] = parsed.get("name")
565
+ stall_name = str(raw_name) if raw_name is not None else ""
566
+ else:
567
+ stall_name = arguments
568
+ except json.JSONDecodeError:
569
+ stall_name = arguments
570
+ else:
571
+ if "arguments" in arguments:
572
+ nested = json.loads(arguments["arguments"])
573
+ if isinstance(nested, dict):
574
+ raw_name = nested.get("name")
575
+ stall_name = str(raw_name) if raw_name is not None else ""
576
+ else:
577
+ raw_name = nested
578
+ stall_name = str(raw_name) if raw_name is not None else ""
579
+ else:
580
+ raw_name = arguments.get("name", arguments)
581
+ stall_name = str(raw_name) if raw_name is not None else ""
582
+
583
+ results = []
584
+ stall_id = None
585
+
586
+ # Find stall ID
587
+ for stall, _ in self.stall_db:
588
+ if stall.name == stall_name:
589
+ stall_id = stall.id
590
+ break
591
+
592
+ if stall_id is None:
593
+ return json.dumps(
594
+ [
595
+ {
596
+ "status": "error",
597
+ "message": f"Stall '{stall_name}' not found in database",
598
+ "stall_name": stall_name,
599
+ }
600
+ ]
601
+ )
602
+
603
+ # Publish products
604
+ for i, (product, _) in enumerate(self.product_db):
605
+ if product.stall_id == stall_id:
606
+ try:
607
+ product_data = product.to_product_data()
608
+ event_id = self._nostr_client.publish_product(product_data)
609
+ self.product_db[i] = (product, event_id)
610
+ results.append(
611
+ {
612
+ "status": "success",
613
+ "event_id": str(event_id),
614
+ "product_name": product.name,
615
+ "stall_name": stall_name,
616
+ }
617
+ )
618
+ except Exception as e:
619
+ results.append(
620
+ {
621
+ "status": "error",
622
+ "message": str(e),
623
+ "product_name": product.name,
624
+ "stall_name": stall_name,
625
+ }
626
+ )
627
+
628
+ if not results:
629
+ return json.dumps(
630
+ [
631
+ {
632
+ "status": "error",
633
+ "message": f"No products found in stall '{stall_name}'",
634
+ "stall_name": stall_name,
635
+ }
636
+ ]
637
+ )
638
+
639
+ return json.dumps(results)
640
+
641
+ except Exception as e:
642
+ return json.dumps(
643
+ [{"status": "error", "message": str(e), "arguments": str(arguments)}]
644
+ )
645
+
646
+ def publish_profile(self) -> str:
647
+ """
648
+ Publishes the profile on Nostr
649
+
650
+ Returns:
651
+ str: JSON of the event that published the profile
652
+
653
+ Raises:
654
+ RuntimeError: if it can't publish the event
655
+ """
656
+ if self._nostr_client is None:
657
+ raise ValueError("NostrClient not initialized")
658
+
659
+ try:
660
+ event_id = self._nostr_client.publish_profile(
141
661
  self.merchant_profile.get_name(),
142
662
  self.merchant_profile.get_about(),
143
- self.merchant_profile.get_picture()
144
- )
145
-
146
- # Check if the operation resulted in an error
147
- if eventid == nostr.NostrClient.ERROR:
148
- return nostr.NostrClient.ERROR
149
-
150
- # Return the event ID and merchant profile details
151
- return eventid + self.merchant_profile.merchant_profile_to_str()
152
-
153
- def get_merchant_url(
154
- self
155
- ) -> str:
663
+ self.merchant_profile.get_picture(),
664
+ )
665
+ return json.dumps(event_id.__dict__)
666
+ except Exception as e:
667
+ raise RuntimeError(f"Unable to publish the profile: {e}")
668
+
669
+ def publish_new_stall(self, stall: MerchantStall) -> str:
156
670
  """
157
- Returns URL with merchant profile
671
+ Publishes a new stall that is not currently in the Merchant's Stall DB and adds it to the Stall DB
672
+
673
+ Args:
674
+ stall: MerchantStall to be published
158
675
 
159
676
  Returns:
160
- str: valid URL with merchant profile
677
+ str: JSON string with status of the operation
161
678
  """
679
+ if self._nostr_client is None:
680
+ raise ValueError("NostrClient not initialized")
681
+
682
+ 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)
687
+ # we need to add the stall event id to the stall db
688
+ self.stall_db.append((stall, event_id))
689
+ return json.dumps(
690
+ {
691
+ "status": "success",
692
+ "event_id": str(event_id),
693
+ "stall_name": stall.name,
694
+ }
695
+ )
696
+ except Exception as e:
697
+ return json.dumps(
698
+ {"status": "error", "message": str(e), "stall_name": stall.name}
699
+ )
700
+
701
+ def publish_stall_by_name(self, arguments: Union[str, dict]) -> str:
702
+ if self._nostr_client is None:
703
+ raise ValueError("NostrClient not initialized")
704
+
705
+ try:
706
+ # Parse arguments to get stall_name
707
+ stall_name: str
708
+ if isinstance(arguments, str):
709
+ try:
710
+ # Try to parse as JSON first
711
+ parsed = json.loads(arguments)
712
+ if isinstance(parsed, dict):
713
+ raw_name: Optional[Any] = parsed.get("name")
714
+ stall_name = str(raw_name) if raw_name is not None else ""
715
+ else:
716
+ stall_name = arguments
717
+ except json.JSONDecodeError:
718
+ # If not JSON, use the raw string
719
+ stall_name = arguments
720
+ else:
721
+ # Handle dict input
722
+ if "arguments" in arguments:
723
+ nested = json.loads(arguments["arguments"])
724
+ if isinstance(nested, dict):
725
+ raw_name = nested.get("name")
726
+ stall_name = str(raw_name) if raw_name is not None else ""
727
+ else:
728
+ raw_name = nested
729
+ stall_name = str(raw_name) if raw_name is not None else ""
730
+ else:
731
+ raw_name = arguments.get("name", arguments)
732
+ stall_name = str(raw_name) if raw_name is not None else ""
733
+
734
+ # Find and publish stall
735
+ for i, (stall, _) in enumerate(self.stall_db):
736
+ if stall.name == stall_name:
737
+ try:
738
+ stall_data = stall.to_stall_data()
739
+ event_id = self._nostr_client.publish_stall(stall_data)
740
+ self.stall_db[i] = (stall, event_id)
741
+ return json.dumps(
742
+ {
743
+ "status": "success",
744
+ "event_id": str(event_id),
745
+ "stall_name": stall.name,
746
+ }
747
+ )
748
+ except Exception as e:
749
+ return json.dumps(
750
+ [
751
+ {
752
+ "status": "error",
753
+ "message": str(e),
754
+ "stall_name": stall.name,
755
+ }
756
+ ]
757
+ )
758
+
759
+ # Stall not found
760
+ return json.dumps(
761
+ [
762
+ {
763
+ "status": "error",
764
+ "message": f"Stall '{stall_name}' not found in database",
765
+ "stall_name": stall_name,
766
+ }
767
+ ]
768
+ )
162
769
 
163
- return self.WEB_URL + self.merchant_profile.get_public_key()
770
+ except Exception as e:
771
+ return json.dumps(
772
+ [{"status": "error", "message": str(e), "stall_name": "unknown"}]
773
+ )
774
+
775
+ def remove_all_products(self) -> str:
776
+ """
777
+ Removes all published products from Nostr
778
+
779
+ Returns:
780
+ str: JSON array with status of all product removal operations
781
+ """
782
+ if self._nostr_client is None:
783
+ raise ValueError("NostrClient not initialized")
784
+
785
+ results = []
786
+
787
+ for i, (product, event_id) in enumerate(self.product_db):
788
+ if event_id is None:
789
+ results.append(
790
+ {
791
+ "status": "skipped",
792
+ "message": f"Product '{product.name}' has not been published yet",
793
+ "product_name": product.name,
794
+ }
795
+ )
796
+ continue
797
+
798
+ try:
799
+ # Delete the event using the SDK's method
800
+ self._nostr_client.delete_event(
801
+ event_id, reason=f"Product '{product.name}' removed"
802
+ )
803
+ # Remove the event_id, keeping the product in the database
804
+ self.product_db[i] = (product, None)
805
+ results.append(
806
+ {
807
+ "status": "success",
808
+ "message": f"Product '{product.name}' removed",
809
+ "product_name": product.name,
810
+ "event_id": str(event_id),
811
+ }
812
+ )
813
+ except Exception as e:
814
+ results.append(
815
+ {"status": "error", "message": str(e), "product_name": product.name}
816
+ )
817
+
818
+ return json.dumps(results)
819
+
820
+ def remove_all_stalls(self) -> str:
821
+ """
822
+ Removes all stalls and their products from Nostr
823
+
824
+ Returns:
825
+ str: JSON array with status of all removal operations
826
+ """
827
+ if self._nostr_client is None:
828
+ raise ValueError("NostrClient not initialized")
829
+
830
+ results = []
831
+
832
+ # First remove all products from all stalls
833
+ for i, (stall, _) in enumerate(self.stall_db):
834
+ stall_name = stall.name
835
+ stall_id = stall.id
836
+
837
+ # Remove all products in this stall
838
+ for j, (product, event_id) in enumerate(self.product_db):
839
+ if product.stall_id == stall_id:
840
+ if event_id is None:
841
+ results.append(
842
+ {
843
+ "status": "skipped",
844
+ "message": f"Product '{product.name}' has not been published yet",
845
+ "product_name": product.name,
846
+ "stall_name": stall_name,
847
+ }
848
+ )
849
+ continue
850
+
851
+ try:
852
+ self._nostr_client.delete_event(
853
+ event_id,
854
+ reason=f"Stall for product '{product.name}' removed",
855
+ )
856
+ self.product_db[j] = (product, None)
857
+ results.append(
858
+ {
859
+ "status": "success",
860
+ "message": f"Product '{product.name}' removed",
861
+ "product_name": product.name,
862
+ "stall_name": stall_name,
863
+ "event_id": str(event_id),
864
+ }
865
+ )
866
+ except Exception as e:
867
+ results.append(
868
+ {
869
+ "status": "error",
870
+ "message": str(e),
871
+ "product_name": product.name,
872
+ "stall_name": stall_name,
873
+ }
874
+ )
875
+
876
+ # Now remove the stall itself
877
+ _, stall_event_id = self.stall_db[i]
878
+ if stall_event_id is None:
879
+ results.append(
880
+ {
881
+ "status": "skipped",
882
+ "message": f"Stall '{stall_name}' has not been published yet",
883
+ "stall_name": stall_name,
884
+ }
885
+ )
886
+ else:
887
+ try:
888
+ self._nostr_client.delete_event(
889
+ stall_event_id, reason=f"Stall '{stall_name}' removed"
890
+ )
891
+ self.stall_db[i] = (stall, None)
892
+ results.append(
893
+ {
894
+ "status": "success",
895
+ "message": f"Stall '{stall_name}' removed",
896
+ "stall_name": stall_name,
897
+ "event_id": str(stall_event_id),
898
+ }
899
+ )
900
+ except Exception as e:
901
+ results.append(
902
+ {"status": "error", "message": str(e), "stall_name": stall_name}
903
+ )
904
+
905
+ return json.dumps(results)
906
+
907
+ def remove_product_by_name(self, arguments: str) -> str:
908
+ """
909
+ Deletes a product with the given name from Nostr
910
+
911
+ Args:
912
+ arguments: JSON string that may contain {"name": "product_name"} or just "product_name"
913
+
914
+ Returns:
915
+ str: JSON string with status of the operation
916
+ """
917
+ if self._nostr_client is None:
918
+ raise ValueError("NostrClient not initialized")
919
+
920
+ try:
921
+ # Try to parse as JSON first
922
+ if isinstance(arguments, dict):
923
+ parsed = arguments
924
+ else:
925
+ parsed = json.loads(arguments)
926
+ name = parsed.get(
927
+ "name", parsed
928
+ ) # Get name if exists, otherwise use whole value
929
+ except json.JSONDecodeError:
930
+ # If not JSON, use the raw string
931
+ name = arguments
932
+
933
+ # Find the product and its event_id in the product_db
934
+ for i, (product, event_id) in enumerate(self.product_db):
935
+ if product.name == name:
936
+ if event_id is None:
937
+ return json.dumps(
938
+ {
939
+ "status": "error",
940
+ "message": f"Product '{name}' has not been published yet",
941
+ "product_name": name,
942
+ }
943
+ )
944
+
945
+ try:
946
+ # Delete the event using the SDK's method
947
+ self._nostr_client.delete_event(
948
+ event_id, reason=f"Product '{name}' removed"
949
+ )
950
+ # Remove the event_id, keeping the product in the database
951
+ self.product_db[i] = (product, None)
952
+ return json.dumps(
953
+ {
954
+ "status": "success",
955
+ "message": f"Product '{name}' removed",
956
+ "product_name": name,
957
+ "event_id": str(event_id),
958
+ }
959
+ )
960
+ except Exception as e:
961
+ return json.dumps(
962
+ {"status": "error", "message": str(e), "product_name": name}
963
+ )
964
+
965
+ # If we get here, we didn't find the product
966
+ return json.dumps(
967
+ {
968
+ "status": "error",
969
+ "message": f"Product '{name}' not found in database",
970
+ "product_name": name,
971
+ }
972
+ )
973
+
974
+ def remove_stall_by_name(self, arguments: Union[str, dict]) -> str:
975
+ """Remove a stall and its products by name
976
+
977
+ Args:
978
+ arguments: str or dict with the stall name. Can be in formats:
979
+ - {"name": "stall_name"}
980
+ - {"arguments": "{\"name\": \"stall_name\"}"}
981
+ - "stall_name"
982
+
983
+ Returns:
984
+ str: JSON array with status of the operation
985
+ """
986
+ if self._nostr_client is None:
987
+ raise ValueError("NostrClient not initialized")
988
+
989
+ try:
990
+ # Parse arguments to get stall_name
991
+ stall_name: str
992
+ if isinstance(arguments, str):
993
+ try:
994
+ parsed = json.loads(arguments)
995
+ if isinstance(parsed, dict):
996
+ raw_name: Optional[Any] = parsed.get("name")
997
+ stall_name = str(raw_name) if raw_name is not None else ""
998
+ else:
999
+ stall_name = arguments
1000
+ except json.JSONDecodeError:
1001
+ stall_name = arguments
1002
+ else:
1003
+ if "arguments" in arguments:
1004
+ nested = json.loads(arguments["arguments"])
1005
+ if isinstance(nested, dict):
1006
+ raw_name = nested.get("name")
1007
+ stall_name = str(raw_name) if raw_name is not None else ""
1008
+ else:
1009
+ raw_name = nested
1010
+ stall_name = str(raw_name) if raw_name is not None else ""
1011
+ else:
1012
+ raw_name = arguments.get("name", arguments)
1013
+ stall_name = str(raw_name) if raw_name is not None else ""
1014
+
1015
+ results = []
1016
+ stall_index = None
1017
+ stall_id = None
1018
+
1019
+ # Find the stall and its event_id in the stall_db
1020
+ for i, (stall, event_id) in enumerate(self.stall_db):
1021
+ if stall.name == stall_name:
1022
+ stall_index = i
1023
+ stall_id = stall.id
1024
+ break
1025
+
1026
+ # If stall_id is empty, then we found no match
1027
+ if stall_id is None:
1028
+ return json.dumps(
1029
+ [
1030
+ {
1031
+ "status": "error",
1032
+ "message": f"Stall '{stall_name}' not found in database",
1033
+ "stall_name": stall_name,
1034
+ }
1035
+ ]
1036
+ )
1037
+
1038
+ # First remove all products in this stall
1039
+ for i, (product, event_id) in enumerate(self.product_db):
1040
+ if product.stall_id == stall_id:
1041
+ if event_id is None:
1042
+ results.append(
1043
+ {
1044
+ "status": "skipped",
1045
+ "message": f"Product '{product.name}' has not been published yet",
1046
+ "product_name": product.name,
1047
+ "stall_name": stall_name,
1048
+ }
1049
+ )
1050
+ continue
1051
+
1052
+ try:
1053
+ self._nostr_client.delete_event(
1054
+ event_id, reason=f"Stall for '{product.name}' removed"
1055
+ )
1056
+ self.product_db[i] = (product, None)
1057
+ results.append(
1058
+ {
1059
+ "status": "success",
1060
+ "message": f"Product '{product.name}' removed",
1061
+ "product_name": product.name,
1062
+ "stall_name": stall_name,
1063
+ "event_id": str(event_id),
1064
+ }
1065
+ )
1066
+ except Exception as e:
1067
+ results.append(
1068
+ {
1069
+ "status": "error",
1070
+ "message": str(e),
1071
+ "product_name": product.name,
1072
+ "stall_name": stall_name,
1073
+ }
1074
+ )
1075
+
1076
+ # Now remove the stall itself
1077
+ if stall_index is not None:
1078
+ _, stall_event_id = self.stall_db[stall_index]
1079
+ if stall_event_id is None:
1080
+ results.append(
1081
+ {
1082
+ "status": "skipped",
1083
+ "message": f"Stall '{stall_name}' has not been published yet",
1084
+ "stall_name": stall_name,
1085
+ }
1086
+ )
1087
+ else:
1088
+ try:
1089
+ self._nostr_client.delete_event(
1090
+ stall_event_id, reason=f"Stall '{stall_name}' removed"
1091
+ )
1092
+ self.stall_db[stall_index] = (
1093
+ self.stall_db[stall_index][0],
1094
+ None,
1095
+ )
1096
+ results.append(
1097
+ {
1098
+ "status": "success",
1099
+ "message": f"Stall '{stall_name}' removed",
1100
+ "stall_name": stall_name,
1101
+ "event_id": str(stall_event_id),
1102
+ }
1103
+ )
1104
+ except Exception as e:
1105
+ results.append(
1106
+ {
1107
+ "status": "error",
1108
+ "message": str(e),
1109
+ "stall_name": stall_name,
1110
+ }
1111
+ )
1112
+
1113
+ return json.dumps(results)
1114
+
1115
+ except Exception as e:
1116
+ return json.dumps(
1117
+ [{"status": "error", "message": str(e), "stall_name": "unknown"}]
1118
+ )
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)