agentstr 0.1.11__py3-none-any.whl → 0.1.13__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|