agentstr 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentstr/__init__.py +36 -17
- agentstr/buyer.py +305 -0
- agentstr/buyer.pyi +32 -0
- agentstr/{marketplace.py → merchant.py} +167 -355
- agentstr/merchant.pyi +37 -0
- agentstr/models.py +380 -0
- agentstr/models.pyi +103 -0
- agentstr/nostr.py +412 -79
- agentstr/nostr.pyi +96 -0
- agentstr/py.typed +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.13.dist-info}/METADATA +42 -56
- agentstr-0.1.13.dist-info/RECORD +15 -0
- agentstr-0.1.11.dist-info/RECORD +0 -8
- {agentstr-0.1.11.dist-info → agentstr-0.1.13.dist-info}/LICENSE +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.13.dist-info}/WHEEL +0 -0
- {agentstr-0.1.11.dist-info → agentstr-0.1.13.dist-info}/top_level.txt +0 -0
agentstr/nostr.py
CHANGED
@@ -1,51 +1,54 @@
|
|
1
|
+
"""
|
2
|
+
Core Nostr utilities for agentstr.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import json
|
1
7
|
import logging
|
2
|
-
from
|
8
|
+
from datetime import timedelta
|
9
|
+
from pathlib import Path
|
10
|
+
from typing import Dict, List, Optional
|
3
11
|
|
4
|
-
|
5
|
-
import asyncio
|
6
|
-
except ImportError:
|
7
|
-
raise ImportError(
|
8
|
-
"`asyncio` not installed. Please install using `pip install asyncio`"
|
9
|
-
)
|
12
|
+
from agentstr.models import MerchantProduct, MerchantStall, NostrProfile
|
10
13
|
|
11
14
|
try:
|
12
15
|
from nostr_sdk import (
|
16
|
+
Alphabet,
|
13
17
|
Client,
|
14
18
|
Coordinate,
|
15
|
-
Event,
|
16
19
|
EventBuilder,
|
17
20
|
EventId,
|
21
|
+
Events,
|
22
|
+
Filter,
|
18
23
|
Keys,
|
19
24
|
Kind,
|
20
25
|
Metadata,
|
21
26
|
NostrSigner,
|
22
27
|
ProductData,
|
23
28
|
PublicKey,
|
24
|
-
|
25
|
-
ShippingMethod,
|
29
|
+
SingleLetterTag,
|
26
30
|
StallData,
|
27
31
|
Tag,
|
28
|
-
|
32
|
+
TagKind,
|
33
|
+
TagStandard,
|
29
34
|
)
|
30
|
-
|
31
|
-
except ImportError:
|
35
|
+
except ImportError as exc:
|
32
36
|
raise ImportError(
|
33
37
|
"`nostr_sdk` not installed. Please install using `pip install nostr_sdk`"
|
34
|
-
)
|
38
|
+
) from exc
|
35
39
|
|
36
40
|
|
37
41
|
class NostrClient:
|
38
42
|
"""
|
39
|
-
NostrClient implements the set of Nostr utilities required for
|
40
|
-
like the Marketplace.
|
43
|
+
NostrClient implements the set of Nostr utilities required for
|
44
|
+
higher level functions implementations like the Marketplace.
|
41
45
|
|
42
|
-
Nostr is an asynchronous communication protocol. To hide this,
|
43
|
-
|
46
|
+
Nostr is an asynchronous communication protocol. To hide this,
|
47
|
+
NostrClient exposes synchronous functions. Users of the NostrClient
|
48
|
+
should ignore `_async_` functions which are for internal purposes only.
|
44
49
|
"""
|
45
50
|
|
46
51
|
logger = logging.getLogger("NostrClient")
|
47
|
-
ERROR: str = "ERROR"
|
48
|
-
SUCCESS: str = "SUCCESS"
|
49
52
|
|
50
53
|
def __init__(
|
51
54
|
self,
|
@@ -74,6 +77,7 @@ class NostrClient:
|
|
74
77
|
self.keys = Keys.parse(nsec)
|
75
78
|
self.nostr_signer = NostrSigner.keys(self.keys)
|
76
79
|
self.client = Client(self.nostr_signer)
|
80
|
+
self.connected = False
|
77
81
|
|
78
82
|
def delete_event(self, event_id: EventId, reason: Optional[str] = None) -> EventId:
|
79
83
|
"""
|
@@ -98,7 +102,7 @@ class NostrClient:
|
|
98
102
|
Publish generic Nostr event to the relay
|
99
103
|
|
100
104
|
Returns:
|
101
|
-
EventId: event id
|
105
|
+
EventId: event id published
|
102
106
|
|
103
107
|
Raises:
|
104
108
|
RuntimeError: if the product can't be published
|
@@ -113,7 +117,7 @@ class NostrClient:
|
|
113
117
|
text: text to be published as kind 1 event
|
114
118
|
|
115
119
|
Returns:
|
116
|
-
EventId: EventId if successful
|
120
|
+
EventId: EventId if successful
|
117
121
|
|
118
122
|
Raises:
|
119
123
|
RuntimeError: if the product can't be published
|
@@ -121,7 +125,7 @@ class NostrClient:
|
|
121
125
|
# Run the async publishing function synchronously
|
122
126
|
return asyncio.run(self._async_publish_note(text))
|
123
127
|
|
124
|
-
def publish_product(self, product:
|
128
|
+
def publish_product(self, product: MerchantProduct) -> EventId:
|
125
129
|
"""
|
126
130
|
Create or update a NIP-15 Marketplace product with event kind 30018
|
127
131
|
|
@@ -129,13 +133,16 @@ class NostrClient:
|
|
129
133
|
product: product to be published
|
130
134
|
|
131
135
|
Returns:
|
132
|
-
EventId: event id
|
136
|
+
EventId: event id of the publication event
|
133
137
|
|
134
138
|
Raises:
|
135
139
|
RuntimeError: if the product can't be published
|
136
140
|
"""
|
137
141
|
# Run the async publishing function synchronously
|
138
|
-
|
142
|
+
try:
|
143
|
+
return asyncio.run(self._async_publish_product(product))
|
144
|
+
except Exception as e:
|
145
|
+
raise RuntimeError(f"Failed to publish product: {e}") from e
|
139
146
|
|
140
147
|
def publish_profile(self, name: str, about: str, picture: str) -> EventId:
|
141
148
|
"""
|
@@ -147,7 +154,7 @@ class NostrClient:
|
|
147
154
|
picture: url to a png file with a picture for the profile
|
148
155
|
|
149
156
|
Returns:
|
150
|
-
EventId: event id if successful
|
157
|
+
EventId: event id if successful
|
151
158
|
|
152
159
|
Raises:
|
153
160
|
RuntimeError: if the profile can't be published
|
@@ -155,20 +162,164 @@ class NostrClient:
|
|
155
162
|
# Run the async publishing function synchronously
|
156
163
|
return asyncio.run(self._async_publish_profile(name, about, picture))
|
157
164
|
|
158
|
-
def publish_stall(self, stall:
|
165
|
+
def publish_stall(self, stall: MerchantStall) -> EventId:
|
159
166
|
"""Publish a stall to nostr
|
160
167
|
|
161
168
|
Args:
|
162
169
|
stall: stall to be published
|
163
170
|
|
164
171
|
Returns:
|
165
|
-
EventId:
|
172
|
+
EventId: Id of the publication event
|
173
|
+
|
174
|
+
Raises:
|
175
|
+
RuntimeError: if the stall can't be published
|
166
176
|
"""
|
167
177
|
try:
|
168
178
|
return asyncio.run(self._async_publish_stall(stall))
|
169
179
|
except Exception as e:
|
170
|
-
|
171
|
-
|
180
|
+
raise RuntimeError(f"Failed to publish stall: {e}") from e
|
181
|
+
|
182
|
+
def retrieve_products_from_seller(self, seller: PublicKey) -> List[MerchantProduct]:
|
183
|
+
"""
|
184
|
+
Retrieve all products from a given seller.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
seller: PublicKey of the seller
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
List[MerchantProduct]: list of products from the seller
|
191
|
+
"""
|
192
|
+
products = []
|
193
|
+
try:
|
194
|
+
events = asyncio.run(self._async_retrieve_products_from_seller(seller))
|
195
|
+
events_list = events.to_vec()
|
196
|
+
for event in events_list:
|
197
|
+
content = json.loads(event.content())
|
198
|
+
product_data = ProductData(
|
199
|
+
id=content.get("id"),
|
200
|
+
stall_id=content.get("stall_id"),
|
201
|
+
name=content.get("name"),
|
202
|
+
description=content.get("description"),
|
203
|
+
images=content.get("images", []),
|
204
|
+
currency=content.get("currency"),
|
205
|
+
price=content.get("price"),
|
206
|
+
quantity=content.get("quantity"),
|
207
|
+
specs=content.get("specs", {}),
|
208
|
+
shipping=content.get("shipping", []),
|
209
|
+
categories=content.get("categories", []),
|
210
|
+
)
|
211
|
+
products.append(MerchantProduct.from_product_data(product_data))
|
212
|
+
return products
|
213
|
+
except Exception as e:
|
214
|
+
raise RuntimeError(f"Failed to retrieve products: {e}") from e
|
215
|
+
|
216
|
+
def retrieve_profile(self, public_key: PublicKey) -> NostrProfile:
|
217
|
+
"""
|
218
|
+
Retrieve a Nostr profile from the relay.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
public_key: bech32 encoded public key of the profile to retrieve
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
NostrProfile: profile of the author
|
225
|
+
|
226
|
+
Raises:
|
227
|
+
RuntimeError: if the profile can't be retrieved
|
228
|
+
"""
|
229
|
+
try:
|
230
|
+
return asyncio.run(self._async_retrieve_profile(public_key))
|
231
|
+
except Exception as e:
|
232
|
+
raise RuntimeError(f"Failed to retrieve profile: {e}") from e
|
233
|
+
|
234
|
+
def retrieve_sellers(self) -> set[NostrProfile]:
|
235
|
+
"""
|
236
|
+
Retrieve all sellers from the relay.
|
237
|
+
Sellers are npubs who have published a stall.
|
238
|
+
Return set may be empty if metadata can't be retrieved for any author.
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
set[NostrProfile]: set of seller profiles
|
242
|
+
(skips authors with missing metadata)
|
243
|
+
"""
|
244
|
+
|
245
|
+
# sellers: set[NostrProfile] = set()
|
246
|
+
|
247
|
+
# First we retrieve all stalls from the relay
|
248
|
+
|
249
|
+
try:
|
250
|
+
events = asyncio.run(self._async_retrieve_all_stalls())
|
251
|
+
except Exception as e:
|
252
|
+
raise RuntimeError(f"Failed to retrieve stalls: {e}") from e
|
253
|
+
|
254
|
+
# Now we search for unique npubs from the list of stalls
|
255
|
+
|
256
|
+
events_list = events.to_vec()
|
257
|
+
authors: Dict[PublicKey, NostrProfile] = {}
|
258
|
+
|
259
|
+
for event in events_list:
|
260
|
+
if event.kind() == Kind(30017):
|
261
|
+
# Is this event the first time we see this author?
|
262
|
+
if event.author() not in authors:
|
263
|
+
# First time we see this author.
|
264
|
+
# Let's add the profile to the dictionary
|
265
|
+
try:
|
266
|
+
profile = asyncio.run(
|
267
|
+
self._async_retrieve_profile(event.author())
|
268
|
+
)
|
269
|
+
# Add profile to the dictionary
|
270
|
+
# associated with the author's PublicKey
|
271
|
+
authors[event.author()] = profile
|
272
|
+
except RuntimeError:
|
273
|
+
continue
|
274
|
+
|
275
|
+
# Now we add locations from the event locations to the profile
|
276
|
+
|
277
|
+
for tag in event.tags().to_vec():
|
278
|
+
standardized_tag = tag.as_standardized()
|
279
|
+
if isinstance(standardized_tag, TagStandard.GEOHASH):
|
280
|
+
string_repr = str(standardized_tag)
|
281
|
+
extracted_geohash = string_repr.split("=")[1].rstrip(
|
282
|
+
")"
|
283
|
+
) # Splitting and removing the closing parenthesis
|
284
|
+
|
285
|
+
profile = authors[event.author()]
|
286
|
+
profile.add_location(extracted_geohash)
|
287
|
+
authors[event.author()] = profile
|
288
|
+
# else:
|
289
|
+
# print(f"Unknown tag: {standardized_tag}")
|
290
|
+
|
291
|
+
# once we're done iterating over the events, we return the set of profiles
|
292
|
+
return set(authors.values())
|
293
|
+
|
294
|
+
def retrieve_stalls_from_seller(self, seller: PublicKey) -> List[StallData]:
|
295
|
+
"""
|
296
|
+
Retrieve all stalls from a given seller.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
seller: PublicKey of the seller
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
List[StallData]: list of stalls from the seller
|
303
|
+
|
304
|
+
Raises:
|
305
|
+
RuntimeError: if the stalls can't be retrieved
|
306
|
+
"""
|
307
|
+
stalls = []
|
308
|
+
try:
|
309
|
+
events = asyncio.run(self._async_retrieve_stalls_from_seller(seller))
|
310
|
+
events_list = events.to_vec()
|
311
|
+
for event in events_list:
|
312
|
+
try:
|
313
|
+
# Parse the content field instead of the whole event
|
314
|
+
content = event.content()
|
315
|
+
stall = StallData.from_json(content)
|
316
|
+
stalls.append(stall)
|
317
|
+
except RuntimeError as e:
|
318
|
+
self.logger.warning("Failed to parse stall data: %s", e)
|
319
|
+
continue
|
320
|
+
return stalls
|
321
|
+
except Exception as e:
|
322
|
+
raise RuntimeError(f"Failed to retrieve stalls: {e}") from e
|
172
323
|
|
173
324
|
@classmethod
|
174
325
|
def set_logging_level(cls, logging_level: int) -> None:
|
@@ -180,29 +331,35 @@ class NostrClient:
|
|
180
331
|
cls.logger.setLevel(logging_level)
|
181
332
|
for handler in cls.logger.handlers:
|
182
333
|
handler.setLevel(logging_level)
|
183
|
-
cls.logger.info(
|
334
|
+
cls.logger.info("Logging level set to %s", logging.getLevelName(logging_level))
|
184
335
|
|
185
|
-
#
|
186
|
-
#
|
187
|
-
#
|
336
|
+
# ----------------------------------------------------------------
|
337
|
+
# internal async functions.
|
338
|
+
# Developers should use synchronous functions above
|
339
|
+
# ----------------------------------------------------------------
|
188
340
|
|
189
|
-
async def _async_connect(self) ->
|
190
|
-
"""
|
341
|
+
async def _async_connect(self) -> None:
|
342
|
+
"""
|
343
|
+
Asynchronous function to add relay to the NostrClient
|
344
|
+
instance and connect to it.
|
191
345
|
|
192
|
-
|
193
|
-
|
346
|
+
|
347
|
+
Raises:
|
348
|
+
RuntimeError: if the relay can't be connected to
|
194
349
|
"""
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
350
|
+
|
351
|
+
if not self.connected:
|
352
|
+
try:
|
353
|
+
await self.client.add_relay(self.relay)
|
354
|
+
NostrClient.logger.info("Relay %s successfully added.", self.relay)
|
355
|
+
await self.client.connect()
|
356
|
+
await asyncio.sleep(2) # give time for slower connections
|
357
|
+
NostrClient.logger.info("Connected to relay.")
|
358
|
+
self.connected = True
|
359
|
+
except Exception as e:
|
360
|
+
raise RuntimeError(
|
361
|
+
f"Unable to connect to relay {self.relay}. Exception: {e}."
|
362
|
+
) from e
|
206
363
|
|
207
364
|
async def _async_publish_event(self, event_builder: EventBuilder) -> EventId:
|
208
365
|
"""
|
@@ -214,27 +371,34 @@ class NostrClient:
|
|
214
371
|
Raises:
|
215
372
|
RuntimeError: if the event can't be published
|
216
373
|
"""
|
217
|
-
|
374
|
+
try:
|
375
|
+
await self._async_connect()
|
218
376
|
|
219
|
-
|
220
|
-
|
377
|
+
# Add debug logging
|
378
|
+
NostrClient.logger.debug("Attempting to publish event: %s", event_builder)
|
379
|
+
NostrClient.logger.debug(
|
380
|
+
"Using keys: %s", self.keys.public_key().to_bech32()
|
381
|
+
)
|
221
382
|
|
222
|
-
|
383
|
+
# Wait for connection and try to publish
|
223
384
|
output = await self.client.send_event_builder(event_builder)
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
)
|
228
|
-
|
229
|
-
|
230
|
-
raise RuntimeError("
|
231
|
-
|
232
|
-
NostrClient.logger.
|
233
|
-
|
234
|
-
)
|
235
|
-
raise RuntimeError(
|
236
|
-
f"NostrClient instance not properly initialized. Exception: {e}."
|
385
|
+
|
386
|
+
# More detailed error handling
|
387
|
+
if not output:
|
388
|
+
raise RuntimeError("No output received from send_event_builder")
|
389
|
+
if len(output.success) == 0:
|
390
|
+
reason = getattr(output, "message", "unknown")
|
391
|
+
raise RuntimeError(f"Event rejected by relay. Reason: {reason}")
|
392
|
+
|
393
|
+
NostrClient.logger.info(
|
394
|
+
"Event published with event id: %s", output.id.to_bech32()
|
237
395
|
)
|
396
|
+
return output.id
|
397
|
+
|
398
|
+
except Exception as e:
|
399
|
+
NostrClient.logger.error("Failed to publish event: %s", str(e))
|
400
|
+
NostrClient.logger.debug("Event details:", exc_info=True)
|
401
|
+
raise RuntimeError(f"Unable to publish event: {str(e)}") from e
|
238
402
|
|
239
403
|
async def _async_publish_note(self, text: str) -> EventId:
|
240
404
|
"""
|
@@ -244,7 +408,7 @@ class NostrClient:
|
|
244
408
|
text: text to be published as kind 1 event
|
245
409
|
|
246
410
|
Returns:
|
247
|
-
EventId: event id if successful
|
411
|
+
EventId: event id if successful
|
248
412
|
|
249
413
|
Raises:
|
250
414
|
RuntimeError: if the event can't be published
|
@@ -252,15 +416,16 @@ class NostrClient:
|
|
252
416
|
event_builder = EventBuilder.text_note(text)
|
253
417
|
return await self._async_publish_event(event_builder)
|
254
418
|
|
255
|
-
async def _async_publish_product(self, product:
|
419
|
+
async def _async_publish_product(self, product: MerchantProduct) -> EventId:
|
256
420
|
"""
|
257
|
-
Asynchronous function to create or update a NIP-15
|
421
|
+
Asynchronous function to create or update a NIP-15
|
422
|
+
Marketplace product with event kind 30018
|
258
423
|
|
259
424
|
Args:
|
260
425
|
product: product to publish
|
261
426
|
|
262
427
|
Returns:
|
263
|
-
EventId: event id if successful
|
428
|
+
EventId: event id if successful
|
264
429
|
|
265
430
|
Raises:
|
266
431
|
RuntimeError: if the product can't be published
|
@@ -271,9 +436,10 @@ class NostrClient:
|
|
271
436
|
|
272
437
|
# EventBuilder.product_data() has a bug with tag handling.
|
273
438
|
# We use the function to create the content field and discard the eventbuilder
|
274
|
-
bad_event_builder = EventBuilder.product_data(product)
|
439
|
+
bad_event_builder = EventBuilder.product_data(product.to_product_data())
|
275
440
|
|
276
|
-
# create an event from bad_event_builder to extract the content -
|
441
|
+
# create an event from bad_event_builder to extract the content -
|
442
|
+
# not broadcasted
|
277
443
|
bad_event = await self.client.sign_event_builder(bad_event_builder)
|
278
444
|
content = bad_event.content()
|
279
445
|
|
@@ -281,7 +447,7 @@ class NostrClient:
|
|
281
447
|
good_event_builder = EventBuilder(Kind(30018), content).tags(
|
282
448
|
[Tag.identifier(product.id), Tag.coordinate(coordinate_tag)]
|
283
449
|
)
|
284
|
-
|
450
|
+
NostrClient.logger.info("Product event: %s", good_event_builder)
|
285
451
|
return await self._async_publish_event(good_event_builder)
|
286
452
|
|
287
453
|
async def _async_publish_profile(
|
@@ -296,7 +462,7 @@ class NostrClient:
|
|
296
462
|
picture: url to a png file with a picture for the profile
|
297
463
|
|
298
464
|
Returns:
|
299
|
-
EventId: event id if successful
|
465
|
+
EventId: event id if successful
|
300
466
|
|
301
467
|
Raises:
|
302
468
|
RuntimeError: if the profile can't be published
|
@@ -308,20 +474,187 @@ class NostrClient:
|
|
308
474
|
event_builder = EventBuilder.metadata(metadata_content)
|
309
475
|
return await self._async_publish_event(event_builder)
|
310
476
|
|
311
|
-
async def _async_publish_stall(self, stall:
|
477
|
+
async def _async_publish_stall(self, stall: MerchantStall) -> EventId:
|
312
478
|
"""
|
313
|
-
Asynchronous function to create or update a NIP-15
|
479
|
+
Asynchronous function to create or update a NIP-15
|
480
|
+
Marketplace stall with event kind 30017
|
314
481
|
|
315
482
|
Args:
|
316
483
|
stall: stall to be published
|
317
484
|
|
318
485
|
Returns:
|
319
|
-
EventId:
|
486
|
+
EventId: Id of the publication event
|
320
487
|
|
321
488
|
Raises:
|
322
489
|
RuntimeError: if the profile can't be published
|
323
490
|
"""
|
324
491
|
|
325
|
-
|
326
|
-
|
492
|
+
# good_event_builder = EventBuilder(Kind(30018), content).tags(
|
493
|
+
# [Tag.identifier(product.id), Tag.coordinate(coordinate_tag)]
|
494
|
+
# )
|
495
|
+
|
496
|
+
NostrClient.logger.info("Merchant Stall: %s", stall)
|
497
|
+
event_builder = EventBuilder.stall_data(stall.to_stall_data()).tags(
|
498
|
+
[
|
499
|
+
Tag.custom(
|
500
|
+
TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.G)),
|
501
|
+
[stall.geohash],
|
502
|
+
),
|
503
|
+
]
|
504
|
+
)
|
327
505
|
return await self._async_publish_event(event_builder)
|
506
|
+
|
507
|
+
async def _async_retrieve_all_stalls(self) -> Events:
|
508
|
+
"""
|
509
|
+
Asynchronous function to retreive all stalls from a relay
|
510
|
+
This function is used internally to find Merchants.
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
Events: events containing all stalls.
|
514
|
+
|
515
|
+
Raises:
|
516
|
+
RuntimeError: if the stalls can't be retrieved
|
517
|
+
"""
|
518
|
+
try:
|
519
|
+
await self._async_connect()
|
520
|
+
except Exception as e:
|
521
|
+
raise RuntimeError("Unable to connect to the relay") from e
|
522
|
+
|
523
|
+
try:
|
524
|
+
events_filter = Filter().kind(Kind(30017))
|
525
|
+
events = await self.client.fetch_events_from(
|
526
|
+
urls=[self.relay], filter=events_filter, timeout=timedelta(seconds=2)
|
527
|
+
)
|
528
|
+
return events
|
529
|
+
except Exception as e:
|
530
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}") from e
|
531
|
+
|
532
|
+
async def _async_retrieve_products_from_seller(self, seller: PublicKey) -> Events:
|
533
|
+
"""
|
534
|
+
Asynchronous function to retrieve the products for a given author
|
535
|
+
|
536
|
+
Args:
|
537
|
+
seller: PublicKey of the seller to retrieve the products for
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
Events: list of events containing the products of the seller
|
541
|
+
|
542
|
+
Raises:
|
543
|
+
RuntimeError: if the products can't be retrieved
|
544
|
+
"""
|
545
|
+
try:
|
546
|
+
await self._async_connect()
|
547
|
+
except Exception as e:
|
548
|
+
raise RuntimeError("Unable to connect to the relay") from e
|
549
|
+
|
550
|
+
try:
|
551
|
+
# print(f"Retrieving products from seller: {seller}")
|
552
|
+
events_filter = Filter().kind(Kind(30018)).authors([seller])
|
553
|
+
events = await self.client.fetch_events_from(
|
554
|
+
urls=[self.relay], filter=events_filter, timeout=timedelta(seconds=2)
|
555
|
+
)
|
556
|
+
return events
|
557
|
+
except Exception as e:
|
558
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}") from e
|
559
|
+
|
560
|
+
async def _async_retrieve_profile(self, author: PublicKey) -> NostrProfile:
|
561
|
+
"""
|
562
|
+
Asynchronous function to retrieve the profile for a given author
|
563
|
+
|
564
|
+
Args:
|
565
|
+
author: PublicKey of the author to retrieve the profile for
|
566
|
+
|
567
|
+
Returns:
|
568
|
+
NostrProfile: profile of the author
|
569
|
+
|
570
|
+
Raises:
|
571
|
+
RuntimeError: if the profile can't be retrieved
|
572
|
+
"""
|
573
|
+
try:
|
574
|
+
await self._async_connect()
|
575
|
+
except Exception as e:
|
576
|
+
raise RuntimeError("Unable to connect to the relay") from e
|
577
|
+
|
578
|
+
try:
|
579
|
+
metadata = await self.client.fetch_metadata(
|
580
|
+
public_key=author, timeout=timedelta(seconds=2)
|
581
|
+
)
|
582
|
+
return NostrProfile.from_metadata(metadata, author)
|
583
|
+
except Exception as e:
|
584
|
+
raise RuntimeError(f"Unable to retrieve metadata: {e}") from e
|
585
|
+
|
586
|
+
async def _async_retrieve_stalls_from_seller(self, seller: PublicKey) -> Events:
|
587
|
+
"""
|
588
|
+
Asynchronous function to retrieve the stall for a given author
|
589
|
+
|
590
|
+
Args:
|
591
|
+
seller: PublicKey of the seller to retrieve the stall for
|
592
|
+
|
593
|
+
Returns:
|
594
|
+
Events: list of events containing the stalls of the seller
|
595
|
+
|
596
|
+
Raises:
|
597
|
+
RuntimeError: if the stall can't be retrieved
|
598
|
+
"""
|
599
|
+
try:
|
600
|
+
await self._async_connect()
|
601
|
+
except Exception as e:
|
602
|
+
raise RuntimeError("Unable to connect to the relay") from e
|
603
|
+
|
604
|
+
try:
|
605
|
+
events_filter = Filter().kind(Kind(30017)).authors([seller])
|
606
|
+
events = await self.client.fetch_events_from(
|
607
|
+
urls=[self.relay], filter=events_filter, timeout=timedelta(seconds=2)
|
608
|
+
)
|
609
|
+
return events
|
610
|
+
except Exception as e:
|
611
|
+
raise RuntimeError(f"Unable to retrieve stalls: {e}") from e
|
612
|
+
|
613
|
+
|
614
|
+
def generate_and_save_keys(env_var: str, env_path: Path) -> Keys:
|
615
|
+
"""Generate new nostr keys and save the private key to .env file.
|
616
|
+
|
617
|
+
Args:
|
618
|
+
env_var: Name of the environment variable to store the key
|
619
|
+
env_path: Path to the .env file. If None, looks for .env in current directory
|
620
|
+
|
621
|
+
Returns:
|
622
|
+
The generated Keys object
|
623
|
+
"""
|
624
|
+
# Generate new keys
|
625
|
+
keys = Keys.generate()
|
626
|
+
nsec = keys.secret_key().to_bech32()
|
627
|
+
|
628
|
+
# Determine .env path
|
629
|
+
if env_path is None:
|
630
|
+
env_path = Path.cwd() / ".env"
|
631
|
+
|
632
|
+
# Read existing .env content
|
633
|
+
env_content = ""
|
634
|
+
if env_path.exists():
|
635
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
636
|
+
env_content = f.read()
|
637
|
+
|
638
|
+
# Check if the env var already exists
|
639
|
+
lines = env_content.splitlines()
|
640
|
+
new_lines = []
|
641
|
+
var_found = False
|
642
|
+
|
643
|
+
for line in lines:
|
644
|
+
if line.startswith(f"{env_var}="):
|
645
|
+
new_lines.append(f"{env_var}={nsec}")
|
646
|
+
var_found = True
|
647
|
+
else:
|
648
|
+
new_lines.append(line)
|
649
|
+
|
650
|
+
# If var wasn't found, add it
|
651
|
+
if not var_found:
|
652
|
+
new_lines.append(f"{env_var}={nsec}")
|
653
|
+
|
654
|
+
# Write back to .env
|
655
|
+
with open(env_path, "w", encoding="utf-8") as f:
|
656
|
+
f.write("\n".join(new_lines))
|
657
|
+
if new_lines: # Add final newline if there's content
|
658
|
+
f.write("\n")
|
659
|
+
|
660
|
+
return keys
|