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