synapse-filecoin-sdk 0.1.0__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.
Files changed (64) hide show
  1. pynapse/__init__.py +6 -0
  2. pynapse/_version.py +1 -0
  3. pynapse/contracts/__init__.py +34 -0
  4. pynapse/contracts/abi_registry.py +11 -0
  5. pynapse/contracts/addresses.json +30 -0
  6. pynapse/contracts/erc20_abi.json +92 -0
  7. pynapse/contracts/errorsAbi.json +933 -0
  8. pynapse/contracts/filecoinPayV1Abi.json +2424 -0
  9. pynapse/contracts/filecoinWarmStorageServiceAbi.json +2363 -0
  10. pynapse/contracts/filecoinWarmStorageServiceStateViewAbi.json +651 -0
  11. pynapse/contracts/generated.py +35 -0
  12. pynapse/contracts/payments_abi.json +205 -0
  13. pynapse/contracts/pdpVerifierAbi.json +1266 -0
  14. pynapse/contracts/providerIdSetAbi.json +161 -0
  15. pynapse/contracts/serviceProviderRegistryAbi.json +1479 -0
  16. pynapse/contracts/sessionKeyRegistryAbi.json +147 -0
  17. pynapse/core/__init__.py +68 -0
  18. pynapse/core/abis.py +25 -0
  19. pynapse/core/chains.py +97 -0
  20. pynapse/core/constants.py +27 -0
  21. pynapse/core/errors.py +22 -0
  22. pynapse/core/piece.py +263 -0
  23. pynapse/core/rand.py +14 -0
  24. pynapse/core/typed_data.py +320 -0
  25. pynapse/core/utils.py +30 -0
  26. pynapse/evm/__init__.py +3 -0
  27. pynapse/evm/client.py +26 -0
  28. pynapse/filbeam/__init__.py +3 -0
  29. pynapse/filbeam/service.py +39 -0
  30. pynapse/payments/__init__.py +17 -0
  31. pynapse/payments/service.py +826 -0
  32. pynapse/pdp/__init__.py +21 -0
  33. pynapse/pdp/server.py +331 -0
  34. pynapse/pdp/types.py +38 -0
  35. pynapse/pdp/verifier.py +82 -0
  36. pynapse/retriever/__init__.py +12 -0
  37. pynapse/retriever/async_chain.py +227 -0
  38. pynapse/retriever/chain.py +209 -0
  39. pynapse/session/__init__.py +12 -0
  40. pynapse/session/key.py +30 -0
  41. pynapse/session/permissions.py +57 -0
  42. pynapse/session/registry.py +90 -0
  43. pynapse/sp_registry/__init__.py +11 -0
  44. pynapse/sp_registry/capabilities.py +25 -0
  45. pynapse/sp_registry/pdp_capabilities.py +102 -0
  46. pynapse/sp_registry/service.py +446 -0
  47. pynapse/sp_registry/types.py +52 -0
  48. pynapse/storage/__init__.py +57 -0
  49. pynapse/storage/async_context.py +682 -0
  50. pynapse/storage/async_manager.py +757 -0
  51. pynapse/storage/context.py +680 -0
  52. pynapse/storage/manager.py +758 -0
  53. pynapse/synapse.py +191 -0
  54. pynapse/utils/__init__.py +25 -0
  55. pynapse/utils/constants.py +25 -0
  56. pynapse/utils/errors.py +3 -0
  57. pynapse/utils/metadata.py +35 -0
  58. pynapse/utils/piece_url.py +16 -0
  59. pynapse/warm_storage/__init__.py +13 -0
  60. pynapse/warm_storage/service.py +513 -0
  61. synapse_filecoin_sdk-0.1.0.dist-info/METADATA +74 -0
  62. synapse_filecoin_sdk-0.1.0.dist-info/RECORD +64 -0
  63. synapse_filecoin_sdk-0.1.0.dist-info/WHEEL +4 -0
  64. synapse_filecoin_sdk-0.1.0.dist-info/licenses/LICENSE.md +228 -0
@@ -0,0 +1,682 @@
1
+ """
2
+ AsyncStorageContext - Async version of StorageContext for Python async/await patterns.
3
+
4
+ Represents a specific Service Provider + DataSet pair with full async support.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import random
9
+ from dataclasses import dataclass, field
10
+ from typing import Callable, Dict, List, Optional, TYPE_CHECKING, Awaitable
11
+
12
+ import httpx
13
+
14
+ from pynapse.core.piece import calculate_piece_cid
15
+ from pynapse.core.typed_data import sign_add_pieces_extra_data, sign_create_dataset_extra_data
16
+ from pynapse.pdp import AsyncPDPServer
17
+ from pynapse.utils.metadata import combine_metadata, metadata_matches, metadata_object_to_entries
18
+
19
+ if TYPE_CHECKING:
20
+ from pynapse.sp_registry import AsyncSPRegistryService, ProviderInfo
21
+ from pynapse.warm_storage import AsyncWarmStorageService
22
+
23
+
24
+ # Size constants
25
+ MIN_UPLOAD_SIZE = 256 # bytes
26
+ MAX_UPLOAD_SIZE = 254 * 1024 * 1024 # 254 MiB
27
+
28
+
29
+ @dataclass
30
+ class AsyncUploadResult:
31
+ """Result of an async upload operation."""
32
+ piece_cid: str
33
+ size: int
34
+ tx_hash: Optional[str] = None
35
+ piece_id: Optional[int] = None
36
+
37
+
38
+ @dataclass
39
+ class AsyncProviderSelectionResult:
40
+ """Result of async provider and dataset selection."""
41
+ provider: "ProviderInfo"
42
+ pdp_endpoint: str
43
+ data_set_id: int # -1 means needs to be created
44
+ client_data_set_id: int
45
+ is_existing: bool
46
+ metadata: Dict[str, str] = field(default_factory=dict)
47
+
48
+
49
+ @dataclass
50
+ class AsyncStorageContextOptions:
51
+ """Options for creating an async storage context."""
52
+ provider_id: Optional[int] = None
53
+ provider_address: Optional[str] = None
54
+ data_set_id: Optional[int] = None
55
+ with_cdn: bool = False
56
+ force_create_data_set: bool = False
57
+ metadata: Optional[Dict[str, str]] = None
58
+ exclude_provider_ids: Optional[List[int]] = None
59
+ # Async callbacks
60
+ on_provider_selected: Optional[Callable[["ProviderInfo"], Awaitable[None]]] = None
61
+ on_data_set_resolved: Optional[Callable[[dict], Awaitable[None]]] = None
62
+
63
+
64
+ class AsyncStorageContext:
65
+ """
66
+ Async storage context for a specific provider and dataset.
67
+
68
+ Use the factory methods `create()` or `create_contexts()` to construct
69
+ instances with proper provider selection and dataset resolution.
70
+
71
+ Example:
72
+ ctx = await AsyncStorageContext.create(
73
+ chain=chain,
74
+ private_key=private_key,
75
+ warm_storage=warm_storage,
76
+ sp_registry=sp_registry,
77
+ )
78
+ result = await ctx.upload(data)
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ pdp_endpoint: str,
84
+ chain,
85
+ private_key: str,
86
+ data_set_id: int,
87
+ client_data_set_id: int,
88
+ provider: Optional["ProviderInfo"] = None,
89
+ with_cdn: bool = False,
90
+ metadata: Optional[Dict[str, str]] = None,
91
+ ) -> None:
92
+ self._pdp = AsyncPDPServer(pdp_endpoint)
93
+ self._pdp_endpoint = pdp_endpoint
94
+ self._chain = chain
95
+ self._private_key = private_key
96
+ self._data_set_id = data_set_id
97
+ self._client_data_set_id = client_data_set_id
98
+ self._provider = provider
99
+ self._with_cdn = with_cdn
100
+ self._metadata = metadata or {}
101
+
102
+ @property
103
+ def data_set_id(self) -> int:
104
+ return self._data_set_id
105
+
106
+ @property
107
+ def client_data_set_id(self) -> int:
108
+ return self._client_data_set_id
109
+
110
+ @property
111
+ def provider(self) -> Optional["ProviderInfo"]:
112
+ return self._provider
113
+
114
+ @property
115
+ def with_cdn(self) -> bool:
116
+ return self._with_cdn
117
+
118
+ @property
119
+ def data_set_metadata(self) -> Dict[str, str]:
120
+ return self._metadata
121
+
122
+ @staticmethod
123
+ def _validate_size(size_bytes: int, context: str = "upload") -> None:
124
+ """Validate data size against limits."""
125
+ if size_bytes < MIN_UPLOAD_SIZE:
126
+ raise ValueError(
127
+ f"Data size {size_bytes} bytes is below minimum allowed size of {MIN_UPLOAD_SIZE} bytes"
128
+ )
129
+ if size_bytes > MAX_UPLOAD_SIZE:
130
+ raise ValueError(
131
+ f"Data size {size_bytes} bytes exceeds maximum allowed size of {MAX_UPLOAD_SIZE} bytes "
132
+ f"({MAX_UPLOAD_SIZE // 1024 // 1024} MiB)"
133
+ )
134
+
135
+ @classmethod
136
+ async def create(
137
+ cls,
138
+ chain,
139
+ private_key: str,
140
+ warm_storage: "AsyncWarmStorageService",
141
+ sp_registry: "AsyncSPRegistryService",
142
+ options: Optional[AsyncStorageContextOptions] = None,
143
+ ) -> "AsyncStorageContext":
144
+ """
145
+ Create an async storage context with smart provider and dataset selection.
146
+
147
+ Args:
148
+ chain: The chain configuration
149
+ private_key: Private key for signing
150
+ warm_storage: AsyncWarmStorageService instance
151
+ sp_registry: AsyncSPRegistryService instance
152
+ options: Optional configuration for context creation
153
+
154
+ Returns:
155
+ A configured AsyncStorageContext instance
156
+ """
157
+ from eth_account import Account
158
+ acct = Account.from_key(private_key)
159
+ client_address = acct.address
160
+
161
+ options = options or AsyncStorageContextOptions()
162
+ requested_metadata = combine_metadata(options.metadata, options.with_cdn)
163
+
164
+ # Resolve provider and dataset
165
+ resolution = await cls._resolve_provider_and_data_set(
166
+ client_address=client_address,
167
+ chain=chain,
168
+ private_key=private_key,
169
+ warm_storage=warm_storage,
170
+ sp_registry=sp_registry,
171
+ options=options,
172
+ requested_metadata=requested_metadata,
173
+ )
174
+
175
+ # Fire callbacks
176
+ if options.on_provider_selected and resolution.provider:
177
+ try:
178
+ await options.on_provider_selected(resolution.provider)
179
+ except Exception:
180
+ pass
181
+
182
+ # Create dataset if needed
183
+ data_set_id = resolution.data_set_id
184
+ client_data_set_id = resolution.client_data_set_id
185
+
186
+ if data_set_id == -1:
187
+ # Need to create a new dataset
188
+ pdp = AsyncPDPServer(resolution.pdp_endpoint)
189
+
190
+ # Use a random client_data_set_id like the TypeScript SDK does
191
+ # This ensures uniqueness and avoids collisions with existing datasets
192
+ from pynapse.core.rand import rand_u256
193
+ next_client_id = rand_u256()
194
+
195
+ # Convert metadata dict to list of {key, value} entries
196
+ metadata_entries = metadata_object_to_entries(requested_metadata)
197
+
198
+ extra_data = sign_create_dataset_extra_data(
199
+ private_key=private_key,
200
+ chain=chain,
201
+ client_data_set_id=next_client_id,
202
+ payee=resolution.provider.payee,
203
+ metadata=metadata_entries,
204
+ )
205
+ resp = await pdp.create_data_set(
206
+ record_keeper=chain.contracts.warm_storage,
207
+ extra_data=extra_data,
208
+ )
209
+ # Wait for creation
210
+ status = await pdp.wait_for_data_set_creation(resp.tx_hash)
211
+ data_set_id = status.data_set_id
212
+ # Get client_data_set_id from the new dataset
213
+ ds_info = await warm_storage.get_data_set(data_set_id)
214
+ client_data_set_id = ds_info.client_data_set_id
215
+
216
+ # Fire dataset resolved callback
217
+ if options.on_data_set_resolved:
218
+ try:
219
+ await options.on_data_set_resolved({
220
+ "is_existing": resolution.is_existing,
221
+ "data_set_id": data_set_id,
222
+ "provider": resolution.provider,
223
+ })
224
+ except Exception:
225
+ pass
226
+
227
+ return cls(
228
+ pdp_endpoint=resolution.pdp_endpoint,
229
+ chain=chain,
230
+ private_key=private_key,
231
+ data_set_id=data_set_id,
232
+ client_data_set_id=client_data_set_id,
233
+ provider=resolution.provider,
234
+ with_cdn=options.with_cdn,
235
+ metadata=requested_metadata,
236
+ )
237
+
238
+ @classmethod
239
+ async def create_contexts(
240
+ cls,
241
+ chain,
242
+ private_key: str,
243
+ warm_storage: "AsyncWarmStorageService",
244
+ sp_registry: "AsyncSPRegistryService",
245
+ count: int = 2,
246
+ options: Optional[AsyncStorageContextOptions] = None,
247
+ ) -> List["AsyncStorageContext"]:
248
+ """
249
+ Create multiple async storage contexts for multi-provider redundancy.
250
+
251
+ Args:
252
+ chain: The chain configuration
253
+ private_key: Private key for signing
254
+ warm_storage: AsyncWarmStorageService instance
255
+ sp_registry: AsyncSPRegistryService instance
256
+ count: Number of contexts to create (default: 2)
257
+ options: Optional configuration for context creation
258
+
259
+ Returns:
260
+ List of configured AsyncStorageContext instances
261
+ """
262
+ contexts: List[AsyncStorageContext] = []
263
+ used_provider_ids: List[int] = []
264
+
265
+ options = options or AsyncStorageContextOptions()
266
+
267
+ for _ in range(count):
268
+ # Build options with exclusions
269
+ ctx_options = AsyncStorageContextOptions(
270
+ provider_id=options.provider_id if not contexts else None,
271
+ provider_address=options.provider_address if not contexts else None,
272
+ data_set_id=options.data_set_id if not contexts else None,
273
+ with_cdn=options.with_cdn,
274
+ force_create_data_set=options.force_create_data_set,
275
+ metadata=options.metadata,
276
+ exclude_provider_ids=(options.exclude_provider_ids or []) + used_provider_ids,
277
+ on_provider_selected=options.on_provider_selected,
278
+ on_data_set_resolved=options.on_data_set_resolved,
279
+ )
280
+
281
+ try:
282
+ ctx = await cls.create(
283
+ chain=chain,
284
+ private_key=private_key,
285
+ warm_storage=warm_storage,
286
+ sp_registry=sp_registry,
287
+ options=ctx_options,
288
+ )
289
+ contexts.append(ctx)
290
+ if ctx.provider:
291
+ used_provider_ids.append(ctx.provider.provider_id)
292
+ except Exception as e:
293
+ # If we can't create more contexts, return what we have
294
+ if not contexts:
295
+ raise
296
+ break
297
+
298
+ return contexts
299
+
300
+ @classmethod
301
+ async def _resolve_provider_and_data_set(
302
+ cls,
303
+ client_address: str,
304
+ chain,
305
+ private_key: str,
306
+ warm_storage: "AsyncWarmStorageService",
307
+ sp_registry: "AsyncSPRegistryService",
308
+ options: AsyncStorageContextOptions,
309
+ requested_metadata: Dict[str, str],
310
+ ) -> AsyncProviderSelectionResult:
311
+ """Resolve provider and dataset based on options."""
312
+
313
+ # 1. If explicit data_set_id provided
314
+ if options.data_set_id is not None and not options.force_create_data_set:
315
+ return await cls._resolve_by_data_set_id(
316
+ data_set_id=options.data_set_id,
317
+ client_address=client_address,
318
+ warm_storage=warm_storage,
319
+ sp_registry=sp_registry,
320
+ )
321
+
322
+ # 2. If explicit provider_id provided
323
+ if options.provider_id is not None:
324
+ return await cls._resolve_by_provider_id(
325
+ provider_id=options.provider_id,
326
+ client_address=client_address,
327
+ warm_storage=warm_storage,
328
+ sp_registry=sp_registry,
329
+ requested_metadata=requested_metadata,
330
+ force_create=options.force_create_data_set,
331
+ )
332
+
333
+ # 3. If explicit provider_address provided
334
+ if options.provider_address is not None:
335
+ provider = await sp_registry.get_provider_by_address(options.provider_address)
336
+ if provider is None:
337
+ raise ValueError(f"Provider {options.provider_address} not found in registry")
338
+ return await cls._resolve_by_provider_id(
339
+ provider_id=provider.provider_id,
340
+ client_address=client_address,
341
+ warm_storage=warm_storage,
342
+ sp_registry=sp_registry,
343
+ requested_metadata=requested_metadata,
344
+ force_create=options.force_create_data_set,
345
+ )
346
+
347
+ # 4. Smart selection
348
+ return await cls._smart_select_provider(
349
+ client_address=client_address,
350
+ warm_storage=warm_storage,
351
+ sp_registry=sp_registry,
352
+ requested_metadata=requested_metadata,
353
+ exclude_provider_ids=options.exclude_provider_ids or [],
354
+ force_create=options.force_create_data_set,
355
+ )
356
+
357
+ @classmethod
358
+ async def _resolve_by_data_set_id(
359
+ cls,
360
+ data_set_id: int,
361
+ client_address: str,
362
+ warm_storage: "AsyncWarmStorageService",
363
+ sp_registry: "AsyncSPRegistryService",
364
+ ) -> AsyncProviderSelectionResult:
365
+ """Resolve using explicit dataset ID."""
366
+ await warm_storage.validate_data_set(data_set_id)
367
+ ds_info = await warm_storage.get_data_set(data_set_id)
368
+
369
+ if ds_info.payer.lower() != client_address.lower():
370
+ raise ValueError(
371
+ f"Data set {data_set_id} is not owned by {client_address} (owned by {ds_info.payer})"
372
+ )
373
+
374
+ provider = await sp_registry.get_provider(ds_info.provider_id)
375
+ if provider is None:
376
+ raise ValueError(f"Provider ID {ds_info.provider_id} for data set {data_set_id} not found")
377
+
378
+ # Get PDP endpoint from provider product info
379
+ pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, provider.provider_id)
380
+ metadata = await warm_storage.get_all_data_set_metadata(data_set_id)
381
+
382
+ return AsyncProviderSelectionResult(
383
+ provider=provider,
384
+ pdp_endpoint=pdp_endpoint,
385
+ data_set_id=data_set_id,
386
+ client_data_set_id=ds_info.client_data_set_id,
387
+ is_existing=True,
388
+ metadata=metadata,
389
+ )
390
+
391
+ @classmethod
392
+ async def _resolve_by_provider_id(
393
+ cls,
394
+ provider_id: int,
395
+ client_address: str,
396
+ warm_storage: "AsyncWarmStorageService",
397
+ sp_registry: "AsyncSPRegistryService",
398
+ requested_metadata: Dict[str, str],
399
+ force_create: bool = False,
400
+ ) -> AsyncProviderSelectionResult:
401
+ """Resolve by provider ID, finding or creating dataset."""
402
+ provider = await sp_registry.get_provider(provider_id)
403
+ if provider is None:
404
+ raise ValueError(f"Provider ID {provider_id} not found in registry")
405
+
406
+ pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, provider_id)
407
+
408
+ if force_create:
409
+ return AsyncProviderSelectionResult(
410
+ provider=provider,
411
+ pdp_endpoint=pdp_endpoint,
412
+ data_set_id=-1,
413
+ client_data_set_id=0,
414
+ is_existing=False,
415
+ metadata=requested_metadata,
416
+ )
417
+
418
+ # Try to find existing dataset for this provider
419
+ try:
420
+ datasets = await warm_storage.get_client_data_sets(client_address)
421
+ for ds in datasets:
422
+ if ds.provider_id == provider_id and ds.pdp_end_epoch == 0:
423
+ # Check metadata match
424
+ ds_metadata = await warm_storage.get_all_data_set_metadata(ds.data_set_id)
425
+ if metadata_matches(ds_metadata, requested_metadata):
426
+ return AsyncProviderSelectionResult(
427
+ provider=provider,
428
+ pdp_endpoint=pdp_endpoint,
429
+ data_set_id=ds.data_set_id,
430
+ client_data_set_id=ds.client_data_set_id,
431
+ is_existing=True,
432
+ metadata=ds_metadata,
433
+ )
434
+ except Exception:
435
+ pass
436
+
437
+ # No matching dataset found, need to create
438
+ return AsyncProviderSelectionResult(
439
+ provider=provider,
440
+ pdp_endpoint=pdp_endpoint,
441
+ data_set_id=-1,
442
+ client_data_set_id=0,
443
+ is_existing=False,
444
+ metadata=requested_metadata,
445
+ )
446
+
447
+ @classmethod
448
+ async def _smart_select_provider(
449
+ cls,
450
+ client_address: str,
451
+ warm_storage: "AsyncWarmStorageService",
452
+ sp_registry: "AsyncSPRegistryService",
453
+ requested_metadata: Dict[str, str],
454
+ exclude_provider_ids: List[int],
455
+ force_create: bool = False,
456
+ ) -> AsyncProviderSelectionResult:
457
+ """Smart provider selection with existing dataset reuse."""
458
+ exclude_set = set(exclude_provider_ids)
459
+
460
+ # First, try to find existing datasets with matching metadata
461
+ if not force_create:
462
+ try:
463
+ datasets = await warm_storage.get_client_data_sets_with_details(client_address)
464
+ # Filter for live, managed datasets with matching metadata
465
+ matching = [
466
+ ds for ds in datasets
467
+ if ds.is_live
468
+ and ds.is_managed
469
+ and ds.pdp_end_epoch == 0
470
+ and ds.provider_id not in exclude_set
471
+ and metadata_matches(ds.metadata, requested_metadata)
472
+ ]
473
+
474
+ # Prefer datasets with pieces, sorted by ID (older first)
475
+ matching.sort(key=lambda ds: (-ds.active_piece_count, ds.data_set_id))
476
+
477
+ for ds in matching:
478
+ provider = await sp_registry.get_provider(ds.provider_id)
479
+ if provider and provider.is_active:
480
+ # Health check: try to ping the PDP endpoint
481
+ pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, ds.provider_id)
482
+ if await cls._ping_provider(pdp_endpoint):
483
+ return AsyncProviderSelectionResult(
484
+ provider=provider,
485
+ pdp_endpoint=pdp_endpoint,
486
+ data_set_id=ds.data_set_id,
487
+ client_data_set_id=ds.client_data_set_id,
488
+ is_existing=True,
489
+ metadata=ds.metadata,
490
+ )
491
+ except Exception:
492
+ pass
493
+
494
+ # No existing dataset, select a new provider
495
+ try:
496
+ approved_ids = await warm_storage.get_approved_provider_ids()
497
+ except Exception:
498
+ approved_ids = []
499
+
500
+ # Filter out excluded providers
501
+ candidate_ids = [pid for pid in approved_ids if pid not in exclude_set]
502
+
503
+ # Shuffle for random selection
504
+ random.shuffle(candidate_ids)
505
+
506
+ # Find a healthy provider
507
+ for pid in candidate_ids:
508
+ try:
509
+ provider = await sp_registry.get_provider(pid)
510
+ if provider and provider.is_active:
511
+ pdp_endpoint = await cls._get_pdp_endpoint(sp_registry, pid)
512
+ if await cls._ping_provider(pdp_endpoint):
513
+ return AsyncProviderSelectionResult(
514
+ provider=provider,
515
+ pdp_endpoint=pdp_endpoint,
516
+ data_set_id=-1,
517
+ client_data_set_id=0,
518
+ is_existing=False,
519
+ metadata=requested_metadata,
520
+ )
521
+ except Exception:
522
+ continue
523
+
524
+ raise ValueError("No approved service providers available")
525
+
526
+ @classmethod
527
+ async def _get_pdp_endpoint(cls, sp_registry: "AsyncSPRegistryService", provider_id: int) -> str:
528
+ """Get the PDP service URL for a provider."""
529
+ try:
530
+ product = await sp_registry.get_provider_with_product(provider_id, 0) # PDP product type
531
+ # Look for serviceURL in capability values
532
+ for i, key in enumerate(product.product.capability_keys):
533
+ if key == "serviceURL" and i < len(product.product_capability_values):
534
+ val = product.product_capability_values[i]
535
+ # Values are returned as bytes from the contract
536
+ if isinstance(val, bytes):
537
+ return val.decode('utf-8')
538
+ return str(val)
539
+ except Exception:
540
+ pass
541
+
542
+ raise ValueError(f"Could not find PDP endpoint for provider {provider_id}")
543
+
544
+ @classmethod
545
+ async def _ping_provider(cls, pdp_endpoint: str, timeout: float = 5.0) -> bool:
546
+ """Health check a provider's PDP endpoint."""
547
+ try:
548
+ async with httpx.AsyncClient(timeout=timeout) as client:
549
+ resp = await client.head(pdp_endpoint)
550
+ return resp.status_code < 500
551
+ except Exception:
552
+ return False
553
+
554
+ async def upload(
555
+ self,
556
+ data: bytes,
557
+ metadata: Optional[Dict[str, str]] = None,
558
+ on_progress: Optional[Callable[[int], Awaitable[None]]] = None,
559
+ on_upload_complete: Optional[Callable[[str], Awaitable[None]]] = None,
560
+ on_pieces_added: Optional[Callable[[str], Awaitable[None]]] = None,
561
+ ) -> AsyncUploadResult:
562
+ """
563
+ Upload data to this storage context asynchronously.
564
+
565
+ Args:
566
+ data: Bytes to upload
567
+ metadata: Optional piece metadata
568
+ on_progress: Async callback for upload progress
569
+ on_upload_complete: Async callback when upload completes
570
+ on_pieces_added: Async callback when pieces are added on-chain
571
+
572
+ Returns:
573
+ AsyncUploadResult with piece CID and transaction info
574
+ """
575
+ self._validate_size(len(data))
576
+
577
+ info = calculate_piece_cid(data)
578
+
579
+ # Upload to PDP server (include padded_piece_size for PieceCIDv1)
580
+ await self._pdp.upload_piece(data, info.piece_cid, info.padded_piece_size)
581
+
582
+ # Wait for piece to be indexed before adding to dataset
583
+ # The PDP server needs time to process and index uploaded pieces
584
+ await self._pdp.wait_for_piece(info.piece_cid, timeout_seconds=60, poll_interval=2)
585
+
586
+ if on_upload_complete:
587
+ try:
588
+ await on_upload_complete(info.piece_cid)
589
+ except Exception:
590
+ pass
591
+
592
+ # Add piece to dataset
593
+ pieces = [(info.piece_cid, [{"key": k, "value": v} for k, v in (metadata or {}).items()])]
594
+ extra_data = sign_add_pieces_extra_data(
595
+ private_key=self._private_key,
596
+ chain=self._chain,
597
+ client_data_set_id=self._client_data_set_id,
598
+ pieces=pieces,
599
+ )
600
+
601
+ add_resp = await self._pdp.add_pieces(self._data_set_id, [info.piece_cid], extra_data)
602
+
603
+ if on_pieces_added:
604
+ try:
605
+ await on_pieces_added(add_resp.tx_hash)
606
+ except Exception:
607
+ pass
608
+
609
+ return AsyncUploadResult(
610
+ piece_cid=info.piece_cid,
611
+ size=info.payload_size,
612
+ tx_hash=add_resp.tx_hash,
613
+ )
614
+
615
+ async def upload_multi(
616
+ self,
617
+ data_items: List[bytes],
618
+ metadata: Optional[Dict[str, str]] = None,
619
+ ) -> List[AsyncUploadResult]:
620
+ """
621
+ Upload multiple pieces in a batch asynchronously.
622
+
623
+ Args:
624
+ data_items: List of byte arrays to upload
625
+ metadata: Optional metadata to apply to all pieces
626
+
627
+ Returns:
628
+ List of AsyncUploadResults
629
+ """
630
+ results = []
631
+ piece_infos = []
632
+
633
+ # Calculate CIDs and upload all pieces
634
+ for data in data_items:
635
+ self._validate_size(len(data))
636
+ info = calculate_piece_cid(data)
637
+ await self._pdp.upload_piece(data, info.piece_cid, info.padded_piece_size)
638
+ piece_infos.append(info)
639
+
640
+ # Wait for all pieces to be indexed before adding to dataset
641
+ for info in piece_infos:
642
+ await self._pdp.wait_for_piece(info.piece_cid, timeout_seconds=60, poll_interval=2)
643
+
644
+ # Batch add pieces
645
+ pieces = [
646
+ (info.piece_cid, [{"key": k, "value": v} for k, v in (metadata or {}).items()])
647
+ for info in piece_infos
648
+ ]
649
+ extra_data = sign_add_pieces_extra_data(
650
+ private_key=self._private_key,
651
+ chain=self._chain,
652
+ client_data_set_id=self._client_data_set_id,
653
+ pieces=pieces,
654
+ )
655
+
656
+ piece_cids = [info.piece_cid for info in piece_infos]
657
+ add_resp = await self._pdp.add_pieces(self._data_set_id, piece_cids, extra_data)
658
+
659
+ for info in piece_infos:
660
+ results.append(AsyncUploadResult(
661
+ piece_cid=info.piece_cid,
662
+ size=info.payload_size,
663
+ tx_hash=add_resp.tx_hash,
664
+ ))
665
+
666
+ return results
667
+
668
+ async def download(self, piece_cid: str) -> bytes:
669
+ """Download a piece by CID asynchronously."""
670
+ return await self._pdp.download_piece(piece_cid)
671
+
672
+ async def has_piece(self, piece_cid: str) -> bool:
673
+ """Check if a piece exists on this provider asynchronously."""
674
+ try:
675
+ await self._pdp.find_piece(piece_cid)
676
+ return True
677
+ except Exception:
678
+ return False
679
+
680
+ async def wait_for_piece(self, piece_cid: str, timeout_seconds: int = 300) -> None:
681
+ """Wait for a piece to be available on this provider asynchronously."""
682
+ await self._pdp.wait_for_piece(piece_cid, timeout_seconds)