agentstr 0.1.7__py3-none-any.whl → 0.1.8__py3-none-any.whl

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