ratio1 3.4.89__py3-none-any.whl → 3.4.91__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.
ratio1/_ver.py CHANGED
@@ -1,4 +1,4 @@
1
- __VER__ = "3.4.89"
1
+ __VER__ = "3.4.91"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  with open("pyproject.toml", "rt") as fd:
ratio1/ipfs/r1fs.py CHANGED
@@ -72,6 +72,7 @@ import random
72
72
  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
73
73
 
74
74
  DEFAULT_SECRET = "ratio1"
75
+ DEFAULT_FILENAME_JSON = "data.json"
75
76
 
76
77
  from threading import Lock
77
78
 
@@ -626,16 +627,49 @@ class R1FSEngine:
626
627
  data,
627
628
  fn=None,
628
629
  secret: str = None,
629
- tempfile=False,
630
+ nonce: int = None,
631
+ use_tempfile=False,
630
632
  show_logs=True,
631
633
  raise_on_error=False,
632
- ) -> bool:
634
+ ) -> str:
633
635
  """
634
636
  Add a JSON object to IPFS.
637
+
638
+ Parameters
639
+ ----------
640
+ data : any
641
+ JSON-serializable data to add to IPFS.
642
+
643
+ fn : str, optional
644
+ Filename to use for the JSON data. If None, uses DEFAULT_FILENAME_JSON
645
+ in a unique temporary directory.
646
+
647
+ secret : str, optional
648
+ Passphrase for AES-GCM encryption. Defaults to 'ratio1'.
649
+
650
+ nonce : int, optional
651
+ Nonce for encryption. If None, a random nonce will be generated.
652
+
653
+ use_tempfile : bool, optional
654
+ If True, use a system temporary file with random name. If False, use specified filename in unique directory.
655
+
656
+ show_logs : bool, optional
657
+ Whether to show logs via self.P / self.Pd. Default is True.
658
+
659
+ raise_on_error : bool, optional
660
+ If True, raise an Exception on command errors. Otherwise, logs them. Default is False.
661
+
662
+ Returns
663
+ -------
664
+ str
665
+ The CID of the added JSON file.
635
666
  """
667
+ unique_dir = None
636
668
  try:
637
- json_data = json.dumps(data)
638
- if tempfile:
669
+ json_data = json.dumps(data, sort_keys=True, separators=(',', ':'))
670
+ self.P(f"JSON data: {json_data}")
671
+
672
+ if use_tempfile:
639
673
  if show_logs:
640
674
  self.Pd("Using tempfile for JSON")
641
675
  with tempfile.NamedTemporaryFile(
@@ -644,21 +678,55 @@ class R1FSEngine:
644
678
  f.write(json_data)
645
679
  fn = f.name
646
680
  else:
647
- fn = self._get_unique_or_complete_upload_name(fn=fn, suffix=".json")
681
+ # Use default filename if none specified
682
+ if fn is None:
683
+ fn = DEFAULT_FILENAME_JSON
684
+
685
+ # Create unique temporary directory for the file
686
+ unique_dir = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex)
687
+ os.makedirs(unique_dir, exist_ok=True)
688
+ fn = os.path.join(unique_dir, fn)
689
+
648
690
  if show_logs:
649
- self.Pd(f"Using unique name for JSON: {fn}")
691
+ self.Pd(f"Using unique directory for JSON: {fn}")
650
692
  with open(fn, "w") as f:
651
693
  f.write(json_data)
652
694
  #end if tempfile
695
+
696
+ if show_logs:
697
+ self.Pd(f"About to call add_file with: file_path={fn}, nonce={nonce}, secret={secret[:10] if secret else 'None'}...")
698
+
653
699
  cid = self.add_file(
654
- file_path=fn, secret=secret, show_logs=show_logs,
700
+ file_path=fn,
701
+ secret=secret,
702
+ nonce=nonce,
703
+ show_logs=show_logs,
655
704
  raise_on_error=raise_on_error
656
705
  )
706
+
707
+ if show_logs:
708
+ self.Pd(f"add_file returned CID: {cid}")
709
+
657
710
  return cid
711
+
658
712
  except Exception as e:
659
713
  if show_logs:
660
714
  self.P(f"Error adding JSON to IPFS: {e}", color='r')
715
+ if raise_on_error:
716
+ raise
661
717
  return None
718
+
719
+ finally:
720
+ # Clean up temporary files and directories
721
+ if fn and os.path.exists(fn):
722
+ os.remove(fn)
723
+ if show_logs:
724
+ self.Pd(f"Cleaned up temporary JSON file: {fn}")
725
+
726
+ if unique_dir and os.path.exists(unique_dir):
727
+ os.rmdir(unique_dir)
728
+ if show_logs:
729
+ self.Pd(f"Cleaned up temporary directory: {unique_dir}")
662
730
 
663
731
 
664
732
  def add_yaml(
@@ -666,7 +734,8 @@ class R1FSEngine:
666
734
  data,
667
735
  fn=None,
668
736
  secret: str = None,
669
- tempfile=False,
737
+ nonce: int = None,
738
+ tempfile=False,
670
739
  show_logs=True,
671
740
  raise_on_error=False,
672
741
  ) -> str:
@@ -689,7 +758,10 @@ class R1FSEngine:
689
758
  with open(fn, "w") as f:
690
759
  f.write(yaml_data)
691
760
  cid = self.add_file(
692
- file_path=fn, secret=secret, show_logs=show_logs,
761
+ file_path=fn,
762
+ secret=secret,
763
+ nonce=nonce,
764
+ show_logs=show_logs,
693
765
  raise_on_error=raise_on_error
694
766
  )
695
767
  return cid
@@ -704,36 +776,93 @@ class R1FSEngine:
704
776
  data,
705
777
  fn=None,
706
778
  secret: str = None,
707
- tempfile=False,
779
+ nonce: int = None,
780
+ use_tempfile=False,
708
781
  show_logs=True,
709
782
  raise_on_error=False,
710
- ) -> bool:
783
+ ) -> str:
711
784
  """
712
785
  Add a Pickle object to IPFS.
786
+
787
+ Parameters
788
+ ----------
789
+ data : any
790
+ Python object to pickle and add to IPFS.
791
+
792
+ fn : str, optional
793
+ Filename to use for the pickle data. If None, generates a unique filename.
794
+
795
+ secret : str, optional
796
+ Passphrase for AES-GCM encryption. Defaults to 'ratio1'.
797
+
798
+ nonce : int, optional
799
+ Nonce for encryption. If None, a random nonce will be generated.
800
+
801
+ use_tempfile : bool, optional
802
+ If True, use a system temporary file with random name. If False, use specified filename.
803
+
804
+ show_logs : bool, optional
805
+ Whether to show logs via self.P / self.Pd. Default is True.
806
+
807
+ raise_on_error : bool, optional
808
+ If True, raise an Exception on command errors. Otherwise, logs them. Default is False.
809
+
810
+ Returns
811
+ -------
812
+ str
813
+ The CID of the added pickle file.
713
814
  """
714
815
  try:
715
816
  import pickle
716
- if tempfile:
817
+ if show_logs:
818
+ self.Pd(f"Pickling data of type: {type(data)}")
819
+
820
+ if use_tempfile:
717
821
  if show_logs:
718
822
  self.Pd("Using tempfile for Pickle")
719
823
  with tempfile.NamedTemporaryFile(mode='wb', suffix='.pkl', delete=False) as f:
720
824
  pickle.dump(data, f)
721
825
  fn = f.name
722
826
  else:
723
- fn = self._get_unique_or_complete_upload_name(fn=fn, suffix=".pkl")
724
- if show_logs:
725
- self.Pd(f"Using unique name for pkl: {fn}")
827
+ if fn is None:
828
+ fn = self._get_unique_or_complete_upload_name(fn=fn, suffix=".pkl")
829
+ if show_logs:
830
+ self.Pd(f"Using unique name for pkl: {fn}")
831
+ else:
832
+ if show_logs:
833
+ self.Pd(f"Using provided filename: {fn}")
726
834
  with open(fn, "wb") as f:
727
835
  pickle.dump(data, f)
836
+
837
+ if show_logs:
838
+ self.Pd(f"About to call add_file with: file_path={fn}, nonce={nonce}, secret={secret[:10] if secret else 'None'}...")
839
+
728
840
  cid = self.add_file(
729
- file_path=fn, secret=secret, show_logs=show_logs,
841
+ file_path=fn,
842
+ secret=secret,
843
+ nonce=nonce,
844
+ show_logs=show_logs,
730
845
  raise_on_error=raise_on_error
731
846
  )
847
+
848
+ if show_logs:
849
+ self.Pd(f"add_file returned CID: {cid}")
850
+
732
851
  return cid
852
+
733
853
  except Exception as e:
734
854
  if show_logs:
735
855
  self.P(f"Error adding Pickle to IPFS: {e}", color='r')
856
+ if raise_on_error:
857
+ raise
736
858
  return None
859
+
860
+ finally:
861
+ # Clean up temporary files
862
+ if 'fn' in locals() and fn and os.path.exists(fn):
863
+ os.remove(fn)
864
+ if show_logs:
865
+ self.Pd(f"Cleaned up temporary pickle file: {fn}")
737
866
 
738
867
 
739
868
  @require_ipfs_started
@@ -801,27 +930,35 @@ class R1FSEngine:
801
930
  key = self._hash_secret(secret) # mandatory passphrase
802
931
 
803
932
  if nonce is None:
804
- nonce = os.urandom(12) # recommended for GCM
933
+ nonce_bytes = os.urandom(12) # recommended for GCM
934
+ if show_logs:
935
+ self.Pd(f"Generated random nonce: {nonce_bytes.hex()}")
805
936
  else:
806
- nonce = random.Random(nonce).randbytes(12)
937
+ original_nonce = nonce
938
+ nonce_bytes = random.Random(nonce).randbytes(12)
807
939
 
808
940
  original_basename = os.path.basename(file_path)
809
941
 
810
942
  # JSON metadata storing the original filename
811
943
  meta_dict = {"filename": original_basename}
812
944
  meta_bytes = json.dumps(meta_dict).encode("utf-8")
945
+
946
+ if show_logs:
947
+ self.Pd(f"Original basename: {original_basename}")
948
+ self.Pd(f"Metadata: {meta_dict}")
949
+ self.Pd(f"Secret hash (first 16 bytes): {key[:16].hex()}")
813
950
 
814
951
  tmp_cipher_path = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex + ".bin")
815
952
 
816
953
  folder_cid = None
817
954
  start_time = time.time()
818
955
  try:
819
- encryptor = Cipher(algorithms.AES(key), modes.GCM(nonce)).encryptor()
956
+ encryptor = Cipher(algorithms.AES(key), modes.GCM(nonce_bytes)).encryptor()
820
957
 
821
958
  chunk_size = 1024 * 1024 # 1 MB chunks
822
959
  with open(file_path, "rb") as fin, open(tmp_cipher_path, "wb") as fout:
823
960
  # [nonce][4-byte-len][metadata][ciphertext][16-byte GCM tag]
824
- fout.write(nonce)
961
+ fout.write(nonce_bytes)
825
962
  meta_len = len(meta_bytes)
826
963
  fout.write(meta_len.to_bytes(4, "big"))
827
964
  # encrypt the metadata and save
@@ -840,10 +977,12 @@ class R1FSEngine:
840
977
  tag = encryptor.tag
841
978
  fout.write(tag)
842
979
  #end with fin, fout
843
-
980
+
844
981
  # Now we IPFS-add the ciphertext
845
982
  output = self.__run_command(["ipfs", "add", "-q", "-w", tmp_cipher_path], show_logs=show_logs)
846
983
  lines = output.strip().split("\n")
984
+ self.P("output lines: ", json.dumps(lines))
985
+
847
986
  if not lines:
848
987
  raise RuntimeError("No output from 'ipfs add -w -q' for ciphertext.")
849
988
  folder_cid = lines[-1].strip()
@@ -1002,11 +1141,19 @@ class R1FSEngine:
1002
1141
 
1003
1142
  # if the folder exists cleanup the content
1004
1143
  if os.path.exists(local_folder):
1005
- if show_logs:
1006
- files = os.listdir(local_folder)
1007
- self.Pd(f"Cleaning up {local_folder} with {files}")
1008
- #end if show_logs
1009
- shutil.rmtree(local_folder)
1144
+ if os.path.isdir(local_folder):
1145
+ if show_logs:
1146
+ files = os.listdir(local_folder)
1147
+ self.Pd(f"Cleaning up {local_folder} with {files}")
1148
+ #end if show_logs
1149
+ shutil.rmtree(local_folder)
1150
+ else:
1151
+ # If it's a file, just remove it
1152
+ if show_logs:
1153
+ self.Pd(f"Removing existing file {local_folder}")
1154
+ #end if show_logs
1155
+ os.remove(local_folder)
1156
+ #end if isdir
1010
1157
  #end if local_folder exists
1011
1158
 
1012
1159
 
@@ -1024,6 +1171,14 @@ class R1FSEngine:
1024
1171
  download_elapsed_time = time.time() - start_time
1025
1172
 
1026
1173
  # Expect exactly one file
1174
+ if not os.path.isdir(local_folder):
1175
+ msg = f"Expected {local_folder} to be a directory after IPFS download, but it's not"
1176
+ if raise_on_error:
1177
+ raise RuntimeError(msg)
1178
+ else:
1179
+ self.P(msg, color='r')
1180
+ return
1181
+
1027
1182
  contents = os.listdir(local_folder)
1028
1183
  if len(contents) != 1:
1029
1184
  msg = f"Expected 1 file in {local_folder}, found {contents}"
@@ -1106,6 +1261,224 @@ class R1FSEngine:
1106
1261
  #end if out_path is not None
1107
1262
  return out_path
1108
1263
 
1264
+ @require_ipfs_started
1265
+ def get_pickle(
1266
+ self,
1267
+ cid: str,
1268
+ local_folder: str = None,
1269
+ secret: str = None,
1270
+ timeout: int = None,
1271
+ pin: bool = True,
1272
+ raise_on_error: bool = False,
1273
+ show_logs: bool = True,
1274
+ ):
1275
+ """
1276
+ Retrieve and deserialize a pickle file from R1FS by CID.
1277
+ Uses get_file under the hood to download and decrypt the file, then loads it with pickle.
1278
+
1279
+ Parameters
1280
+ ----------
1281
+ cid : str
1282
+ The folder CID (wrapped single file).
1283
+
1284
+ local_folder : str, optional
1285
+ Destination folder. If None, we default to something like self.__downloads_dir/<CID>.
1286
+
1287
+ secret : str, optional
1288
+ Passphrase for AES-GCM. Must not be empty. Defaults to 'ratio1'.
1289
+
1290
+ timeout : int, optional
1291
+ Maximum seconds for the IPFS get. If None, use IPFSCt.TIMEOUT.
1292
+
1293
+ pin : bool, optional
1294
+ If True, we optionally pin the folder. Default True.
1295
+
1296
+ raise_on_error : bool, optional
1297
+ If True, raise an Exception on command errors/timeouts. Otherwise logs them. Default False.
1298
+
1299
+ show_logs : bool, optional
1300
+ If True, logs steps via self.P / self.Pd. Default True.
1301
+
1302
+ Returns
1303
+ -------
1304
+ any
1305
+ The deserialized Python object from the pickle file.
1306
+
1307
+ Raises
1308
+ ------
1309
+ ValueError
1310
+ If the secret is empty.
1311
+
1312
+ RuntimeError
1313
+ If multiple or zero files are found in the downloaded folder,
1314
+ or if we fail to parse the JSON metadata.
1315
+
1316
+ Exception
1317
+ If the GCM tag is invalid or the IPFS command times out
1318
+ and raise_on_error=True.
1319
+
1320
+ Examples
1321
+ --------
1322
+ >>> # Simple usage with default passphrase
1323
+ >>> data = engine.get_pickle("QmEncFolderXYZ")
1324
+ >>> print(data)
1325
+ {'key': 'value', 'list': [1, 2, 3]}
1326
+
1327
+ """
1328
+ import pickle
1329
+
1330
+ # Use get_file to download and decrypt the file
1331
+ file_path = self.get_file(
1332
+ cid=cid,
1333
+ local_folder=local_folder,
1334
+ secret=secret,
1335
+ timeout=timeout,
1336
+ pin=pin,
1337
+ raise_on_error=raise_on_error,
1338
+ show_logs=show_logs,
1339
+ return_absolute_path=True
1340
+ )
1341
+
1342
+ if file_path is None:
1343
+ if raise_on_error:
1344
+ raise RuntimeError(f"Failed to retrieve file for CID {cid}")
1345
+ else:
1346
+ if show_logs:
1347
+ self.P(f"Failed to retrieve file for CID {cid}", color='r')
1348
+ return None
1349
+
1350
+ # Load the pickle file
1351
+ try:
1352
+ with open(file_path, "rb") as f:
1353
+ data = pickle.load(f)
1354
+
1355
+ if show_logs:
1356
+ self.Pd(f"Successfully loaded pickle data from {file_path}")
1357
+
1358
+ # Clean up the temporary file
1359
+ os.remove(file_path)
1360
+ if show_logs:
1361
+ self.Pd(f"Cleaned up temporary pickle file: {file_path}")
1362
+
1363
+ return data
1364
+
1365
+ except Exception as e:
1366
+ msg = f"Error loading pickle file {file_path}: {e}"
1367
+ if raise_on_error:
1368
+ raise RuntimeError(msg)
1369
+ else:
1370
+ if show_logs:
1371
+ self.P(msg, color='r')
1372
+ return None
1373
+
1374
+ @require_ipfs_started
1375
+ def get_json(
1376
+ self,
1377
+ cid: str,
1378
+ local_folder: str = None,
1379
+ secret: str = None,
1380
+ timeout: int = None,
1381
+ pin: bool = True,
1382
+ raise_on_error: bool = False,
1383
+ show_logs: bool = True,
1384
+ ):
1385
+ """
1386
+ Retrieve and deserialize a JSON file from R1FS by CID.
1387
+ Uses get_file under the hood to download and decrypt the file, then loads it with json.
1388
+
1389
+ Parameters
1390
+ ----------
1391
+ cid : str
1392
+ The folder CID (wrapped single file).
1393
+
1394
+ local_folder : str, optional
1395
+ Destination folder. If None, we default to something like self.__downloads_dir/<CID>.
1396
+
1397
+ secret : str, optional
1398
+ Passphrase for AES-GCM. Must not be empty. Defaults to 'ratio1'.
1399
+
1400
+ timeout : int, optional
1401
+ Maximum seconds for the IPFS get. If None, use IPFSCt.TIMEOUT.
1402
+
1403
+ pin : bool, optional
1404
+ If True, we optionally pin the folder. Default True.
1405
+
1406
+ raise_on_error : bool, optional
1407
+ If True, raise an Exception on command errors/timeouts. Otherwise logs them. Default False.
1408
+
1409
+ show_logs : bool, optional
1410
+ If True, logs steps via self.P / self.Pd. Default True.
1411
+
1412
+ Returns
1413
+ -------
1414
+ any
1415
+ The deserialized JSON object from the file.
1416
+
1417
+ Raises
1418
+ ------
1419
+ ValueError
1420
+ If the secret is empty.
1421
+
1422
+ RuntimeError
1423
+ If multiple or zero files are found in the downloaded folder,
1424
+ or if we fail to parse the JSON metadata.
1425
+
1426
+ Exception
1427
+ If the GCM tag is invalid or the IPFS command times out
1428
+ and raise_on_error=True.
1429
+
1430
+ Examples
1431
+ --------
1432
+ >>> # Simple usage with default passphrase
1433
+ >>> data = engine.get_json("QmEncFolderXYZ")
1434
+ >>> print(data)
1435
+ {'key': 'value', 'list': [1, 2, 3]}
1436
+
1437
+ """
1438
+ # Use get_file to download and decrypt the file
1439
+ file_path = self.get_file(
1440
+ cid=cid,
1441
+ local_folder=local_folder,
1442
+ secret=secret,
1443
+ timeout=timeout,
1444
+ pin=pin,
1445
+ raise_on_error=raise_on_error,
1446
+ show_logs=show_logs,
1447
+ return_absolute_path=True
1448
+ )
1449
+
1450
+ if file_path is None:
1451
+ if raise_on_error:
1452
+ raise RuntimeError(f"Failed to retrieve file for CID {cid}")
1453
+ else:
1454
+ if show_logs:
1455
+ self.P(f"Failed to retrieve file for CID {cid}", color='r')
1456
+ return None
1457
+
1458
+ # Load the JSON file
1459
+ try:
1460
+ with open(file_path, "r", encoding="utf-8") as f:
1461
+ data = json.load(f)
1462
+
1463
+ if show_logs:
1464
+ self.Pd(f"Successfully loaded JSON data from {file_path}")
1465
+
1466
+ # Clean up the temporary file
1467
+ os.remove(file_path)
1468
+ if show_logs:
1469
+ self.Pd(f"Cleaned up temporary JSON file: {file_path}")
1470
+
1471
+ return data
1472
+
1473
+ except Exception as e:
1474
+ msg = f"Error loading JSON file {file_path}: {e}"
1475
+ if raise_on_error:
1476
+ raise RuntimeError(msg)
1477
+ else:
1478
+ if show_logs:
1479
+ self.P(msg, color='r')
1480
+ return None
1481
+
1109
1482
 
1110
1483
  @require_ipfs_started
1111
1484
  def list_pins(self):
@@ -1148,6 +1521,346 @@ class R1FSEngine:
1148
1521
  except Exception as e:
1149
1522
  result = False
1150
1523
  return result
1524
+
1525
+ def calculate_file_cid(
1526
+ self,
1527
+ file_path: str,
1528
+ nonce: int,
1529
+ secret: str = None,
1530
+ show_logs: bool = True
1531
+ ) -> str:
1532
+ """
1533
+ Calculate the CID (Content Identifier) of a file without adding it to IPFS.
1534
+ This method encrypts the file the same way as add_file and computes the CID
1535
+ that would be generated if the encrypted file were added to IPFS.
1536
+
1537
+ Parameters
1538
+ ----------
1539
+ file_path : str
1540
+ Path to the local file to calculate CID for.
1541
+
1542
+ nonce : int
1543
+ Nonce for encryption. Required parameter for deterministic CID calculation.
1544
+
1545
+ secret : str, optional
1546
+ Passphrase for AES-GCM encryption. Defaults to 'ratio1'.
1547
+
1548
+ show_logs : bool, optional
1549
+ Whether to show logs via self.P / self.Pd. Default is True.
1550
+
1551
+ Returns
1552
+ -------
1553
+ str
1554
+ The CID of the encrypted file (e.g., "QmHash...")
1555
+
1556
+ Raises
1557
+ ------
1558
+ FileNotFoundError
1559
+ If file_path does not exist.
1560
+
1561
+ ImportError
1562
+ If IPFS is not available or not running.
1563
+
1564
+ ValueError
1565
+ If nonce is None or invalid.
1566
+
1567
+ Examples
1568
+ --------
1569
+ >>> cid = engine.calculate_file_cid("/path/to/file.txt", nonce=12345)
1570
+ >>> print(cid)
1571
+ QmHash123ABC...
1572
+ """
1573
+ if not os.path.isfile(file_path):
1574
+ raise FileNotFoundError(f"File not found: {file_path}")
1575
+
1576
+ if secret in ["", None]:
1577
+ secret = self.__DEFAULT_SECRET
1578
+
1579
+ # Validate nonce parameter
1580
+ if nonce is None:
1581
+ raise ValueError("nonce parameter is required for deterministic CID calculation")
1582
+
1583
+ # Check file size and throw an error if larger than 2 GB.
1584
+ file_size = os.path.getsize(file_path)
1585
+ if file_size > 2 * 1024 * 1024 * 1024:
1586
+ raise ValueError(f"File {file_path} is too large ({file_size} bytes). Maximum allowed size is 2 GB.")
1587
+
1588
+ key = self._hash_secret(secret)
1589
+ nonce_bytes = random.Random(nonce).randbytes(12)
1590
+
1591
+ original_basename = os.path.basename(file_path)
1592
+
1593
+ # JSON metadata storing the original filename
1594
+ meta_dict = {"filename": original_basename}
1595
+ meta_bytes = json.dumps(meta_dict).encode("utf-8")
1596
+
1597
+ if show_logs:
1598
+ self.Pd(f"Calculating CID for encrypted file: {file_path}")
1599
+ self.Pd(f"Nonce input: {nonce}, Generated nonce bytes: {nonce_bytes.hex()}")
1600
+ self.Pd(f"Original basename: {original_basename}")
1601
+ self.Pd(f"Metadata: {meta_dict}")
1602
+ self.Pd(f"Secret hash (first 16 bytes): {key[:16].hex()}")
1603
+
1604
+ # Create temporary encrypted file (same as in add_file)
1605
+ tmp_cipher_path = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex + ".bin")
1606
+
1607
+ try:
1608
+ # Encrypt the file content (same as in add_file)
1609
+ encryptor = Cipher(algorithms.AES(key), modes.GCM(nonce_bytes)).encryptor()
1610
+
1611
+ chunk_size = 1024 * 1024 # 1 MB chunks
1612
+ with open(file_path, "rb") as fin, open(tmp_cipher_path, "wb") as fout:
1613
+ # [nonce][4-byte-len][metadata][ciphertext][16-byte GCM tag]
1614
+ fout.write(nonce_bytes)
1615
+ meta_len = len(meta_bytes)
1616
+ fout.write(meta_len.to_bytes(4, "big"))
1617
+ # encrypt the metadata and save
1618
+ enc_meta_data = encryptor.update(meta_bytes)
1619
+ fout.write(enc_meta_data)
1620
+
1621
+ while True:
1622
+ chunk = fin.read(chunk_size)
1623
+ if not chunk:
1624
+ break
1625
+ fout.write(encryptor.update(chunk))
1626
+ #end while there are still bytes to read
1627
+ final_ct = encryptor.finalize()
1628
+ fout.write(final_ct)
1629
+ # Append 16-byte GCM tag
1630
+ tag = encryptor.tag
1631
+ fout.write(tag)
1632
+ #end with fin, fout
1633
+
1634
+ # Use IPFS to calculate the hash without adding the file
1635
+ output = self.__run_command(["ipfs", "add", "--only-hash", "-q", "-w", tmp_cipher_path], show_logs=show_logs)
1636
+ lines = output.strip().split("\n")
1637
+ self.P("output lines: ", json.dumps(lines))
1638
+ if not lines:
1639
+ raise RuntimeError("No output from 'ipfs add --only-hash -q -w' for ciphertext.")
1640
+
1641
+ # Get the CID from IPFS
1642
+ file_cid = lines[0].strip()
1643
+ if show_logs:
1644
+ self.Pd(f"IPFS calculated CID: {file_cid}")
1645
+
1646
+ return file_cid
1647
+
1648
+ finally:
1649
+ # Clean up temporary encrypted file
1650
+ if os.path.exists(tmp_cipher_path):
1651
+ os.remove(tmp_cipher_path)
1652
+
1653
+ def calculate_json_cid(
1654
+ self,
1655
+ data,
1656
+ nonce: int,
1657
+ fn: str = None,
1658
+ secret: str = None,
1659
+ show_logs: bool = True
1660
+ ) -> str:
1661
+ """
1662
+ Calculate the CID (Content Identifier) of JSON data without adding it to IPFS.
1663
+ This method saves the JSON data to a temporary file, then uses calculate_file_cid
1664
+ to compute the CID that would be generated if the encrypted file were added to IPFS.
1665
+
1666
+ Parameters
1667
+ ----------
1668
+ data : any
1669
+ JSON-serializable data to calculate CID for.
1670
+
1671
+ nonce : int
1672
+ Nonce for encryption. Required parameter for deterministic CID calculation.
1673
+
1674
+ fn : str, optional
1675
+ Filename to use for the JSON data. If None, uses DEFAULT_FILENAME_JSON.
1676
+ This affects the CID calculation as the filename is included in the metadata.
1677
+
1678
+ secret : str, optional
1679
+ Passphrase for AES-GCM encryption. Defaults to 'ratio1'.
1680
+
1681
+ show_logs : bool, optional
1682
+ Whether to show logs via self.P / self.Pd. Default is True.
1683
+
1684
+ Returns
1685
+ -------
1686
+ str
1687
+ The CID of the encrypted JSON file (e.g., "QmHash...")
1688
+
1689
+ Raises
1690
+ ------
1691
+ ImportError
1692
+ If IPFS is not available or not running.
1693
+
1694
+ ValueError
1695
+ If nonce is None or invalid, or if data cannot be JSON serialized.
1696
+
1697
+ Examples
1698
+ --------
1699
+ >>> data = {"name": "test", "value": 123}
1700
+ >>> cid = engine.calculate_json_cid(data, nonce=12345)
1701
+ >>> print(cid)
1702
+ QmHash123ABC...
1703
+
1704
+ >>> # Use custom filename
1705
+ >>> cid = engine.calculate_json_cid(data, nonce=12345, fn="config.json")
1706
+ >>> print(cid)
1707
+ QmHash456DEF...
1708
+ """
1709
+ # Serialize JSON data
1710
+ try:
1711
+ json_data = json.dumps(data, sort_keys=True, separators=(',', ':'))
1712
+ except (TypeError, ValueError) as e:
1713
+ raise ValueError(f"Data cannot be JSON serialized: {e}")
1714
+
1715
+ self.P(f"JSON data: {json_data}")
1716
+
1717
+ # Use custom filename or default
1718
+ if fn is None:
1719
+ fn = DEFAULT_FILENAME_JSON
1720
+
1721
+ if show_logs:
1722
+ self.Pd(f"Calculating CID for JSON data with {len(json_data)} characters, filename: {fn}")
1723
+
1724
+ # Create temporary file for JSON data with the desired filename in a unique directory
1725
+ unique_dir = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex)
1726
+ os.makedirs(unique_dir, exist_ok=True)
1727
+ tmp_json_path = os.path.join(unique_dir, fn)
1728
+
1729
+ file_cid = None
1730
+ try:
1731
+ # Write JSON data to temporary file
1732
+ with open(tmp_json_path, "w") as f:
1733
+ f.write(json_data)
1734
+
1735
+ if show_logs:
1736
+ self.Pd(f"About to call calculate_file_cid with: file_path={tmp_json_path}, nonce={nonce}, secret={secret[:10] if secret else 'None'}...")
1737
+
1738
+ # Use calculate_file_cid to get the CID
1739
+ file_cid = self.calculate_file_cid(
1740
+ file_path=tmp_json_path,
1741
+ nonce=nonce,
1742
+ secret=secret,
1743
+ show_logs=True # Don't show logs from calculate_file_cid
1744
+ )
1745
+
1746
+ if show_logs:
1747
+ self.Pd(f"calculate_file_cid returned CID: {file_cid}")
1748
+
1749
+ finally:
1750
+ # Clean up temporary JSON file and directory
1751
+ if os.path.exists(tmp_json_path):
1752
+ os.remove(tmp_json_path)
1753
+ if os.path.exists(unique_dir):
1754
+ os.rmdir(unique_dir)
1755
+
1756
+ return file_cid
1757
+
1758
+ def calculate_pickle_cid(
1759
+ self,
1760
+ data,
1761
+ nonce: int,
1762
+ fn: str = None,
1763
+ secret: str = None,
1764
+ show_logs: bool = True
1765
+ ) -> str:
1766
+ """
1767
+ Calculate the CID (Content Identifier) of pickle data without adding it to IPFS.
1768
+ This method saves the pickle data to a temporary file, then uses calculate_file_cid
1769
+ to compute the CID that would be generated if the encrypted file were added to IPFS.
1770
+
1771
+ Parameters
1772
+ ----------
1773
+ data : any
1774
+ Python object to pickle and calculate CID for.
1775
+
1776
+ nonce : int
1777
+ Nonce for encryption. Required parameter for deterministic CID calculation.
1778
+
1779
+ fn : str, optional
1780
+ Filename to use for the pickle data. If None, generates a unique filename.
1781
+ This affects the CID calculation as the filename is included in the metadata.
1782
+
1783
+ secret : str, optional
1784
+ Passphrase for AES-GCM encryption. Defaults to 'ratio1'.
1785
+
1786
+ show_logs : bool, optional
1787
+ Whether to show logs via self.P / self.Pd. Default is True.
1788
+
1789
+ Returns
1790
+ -------
1791
+ str
1792
+ The CID of the encrypted pickle file (e.g., "QmHash...")
1793
+
1794
+ Raises
1795
+ ------
1796
+ ImportError
1797
+ If IPFS is not available or not running.
1798
+
1799
+ ValueError
1800
+ If nonce is None or invalid, or if data cannot be pickled.
1801
+
1802
+ Examples
1803
+ --------
1804
+ >>> data = {"name": "test", "value": 123}
1805
+ >>> cid = engine.calculate_pickle_cid(data, nonce=12345)
1806
+ >>> print(cid)
1807
+ QmHash123ABC...
1808
+
1809
+ >>> # Use custom filename
1810
+ >>> cid = engine.calculate_pickle_cid(data, nonce=12345, fn="model.pkl")
1811
+ >>> print(cid)
1812
+ QmHash456DEF...
1813
+ """
1814
+ # Serialize pickle data
1815
+ try:
1816
+ import pickle
1817
+ pickle_data = pickle.dumps(data)
1818
+ except (TypeError, ValueError, pickle.PicklingError) as e:
1819
+ raise ValueError(f"Data cannot be pickled: {e}")
1820
+
1821
+ if show_logs:
1822
+ self.Pd(f"Pickled data of type: {type(data)}, size: {len(pickle_data)} bytes")
1823
+
1824
+ # Use custom filename or generate unique one
1825
+ if fn is None:
1826
+ fn = self._get_unique_or_complete_upload_name(fn=fn, suffix=".pkl")
1827
+
1828
+ if show_logs:
1829
+ self.Pd(f"Calculating CID for pickle data with {len(pickle_data)} bytes, filename: {fn}")
1830
+
1831
+ # Create temporary file for pickle data with the desired filename in a unique directory
1832
+ unique_dir = os.path.join(tempfile.gettempdir(), uuid.uuid4().hex)
1833
+ os.makedirs(unique_dir, exist_ok=True)
1834
+ tmp_pickle_path = os.path.join(unique_dir, fn)
1835
+
1836
+ file_cid = None
1837
+ try:
1838
+ # Write pickle data to temporary file
1839
+ with open(tmp_pickle_path, "wb") as f:
1840
+ f.write(pickle_data)
1841
+
1842
+ if show_logs:
1843
+ self.Pd(f"About to call calculate_file_cid with: file_path={tmp_pickle_path}, nonce={nonce}, secret={secret[:10] if secret else 'None'}...")
1844
+
1845
+ # Use calculate_file_cid to get the CID
1846
+ file_cid = self.calculate_file_cid(
1847
+ file_path=tmp_pickle_path,
1848
+ nonce=nonce,
1849
+ secret=secret,
1850
+ show_logs=True # Don't show logs from calculate_file_cid
1851
+ )
1852
+
1853
+ if show_logs:
1854
+ self.Pd(f"calculate_file_cid returned CID: {file_cid}")
1855
+
1856
+ finally:
1857
+ # Clean up temporary pickle file and directory
1858
+ if os.path.exists(tmp_pickle_path):
1859
+ os.remove(tmp_pickle_path)
1860
+ if os.path.exists(unique_dir):
1861
+ os.rmdir(unique_dir)
1862
+
1863
+ return file_cid
1151
1864
 
1152
1865
 
1153
1866
  # Start/stop IPFS methods (R1FS API)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ratio1
3
- Version: 3.4.89
3
+ Version: 3.4.91
4
4
  Summary: `ratio1` or Ration1 SDK is the Python SDK required for client app development for the Ratio1 ecosystem
5
5
  Project-URL: Homepage, https://github.com/Ratio1/ratio1_sdk
6
6
  Project-URL: Bug Tracker, https://github.com/Ratio1/ratio1_sdk/issues
@@ -1,5 +1,5 @@
1
1
  ratio1/__init__.py,sha256=YimqgDbjLuywsf8zCWE0EaUXH4MBUrqLxt0TDV558hQ,632
2
- ratio1/_ver.py,sha256=bS5ZQ9tEc590f2BLSsupExDzIClyYkphrGOF6IuPHfU,331
2
+ ratio1/_ver.py,sha256=CG1uVx6N_JWkoDGCIqSi6Ya8ker6PN2LEgvsHIxDnHo,331
3
3
  ratio1/base_decentra_object.py,sha256=iXvAAf6wPnGWzeeiRfwLojVoan-m1e_VsyPzjUQuENo,4492
4
4
  ratio1/plugins_manager_mixin.py,sha256=X1JdGLDz0gN1rPnTN_5mJXR8JmqoBFQISJXmPR9yvCo,11106
5
5
  ratio1/base/__init__.py,sha256=hACh83_cIv7-PwYMM3bQm2IBmNqiHw-3PAfDfAEKz9A,259
@@ -70,7 +70,7 @@ ratio1/io_formatter/default/aixp1.py,sha256=MX0TeUR4APA-qN3vUC6uzcz8Pssz5lgrQWo7
70
70
  ratio1/io_formatter/default/default.py,sha256=gEy78cP2D5s0y8vQh4aHuxqz7D10gGfuiKF311QhrpE,494
71
71
  ratio1/ipfs/__init__.py,sha256=vXEDLUNUO6lOTMGa8iQ9Zf7ajIQq9GZuvYraAHt3meE,38
72
72
  ratio1/ipfs/ifps_keygen,sha256=PcoYuo4c89_C9FWrKq9K_28ruhKqnxNn1s3nLHiF1tc,879
73
- ratio1/ipfs/r1fs.py,sha256=bslLpr0FTiqBsXPCoAqmH62MDki1lkvgW-oT7TvgFcQ,49458
73
+ ratio1/ipfs/r1fs.py,sha256=5Uo_adugcGTg8kVsev9kmyUSSzh9FdM803jPdu8QEWw,72273
74
74
  ratio1/ipfs/ipfs_setup/ipfs.service,sha256=isTJQsktPy4i1yaDA9AC1OKdlTYvsCCRRAVX-EmGqAs,248
75
75
  ratio1/ipfs/ipfs_setup/launch_service.sh,sha256=GWhZyNqtohLxJg8Q_c8YnNZduu1ddXDU-IFRRMaEyiY,141
76
76
  ratio1/ipfs/ipfs_setup/restart.sh,sha256=9xHMgkUoAMI25jeaoDVFbCa_LjojYm3ubljW58RatKE,22
@@ -105,8 +105,8 @@ ratio1/utils/comm_utils.py,sha256=4cS9llRr_pK_3rNgDcRMCQwYPO0kcNU7AdWy_LtMyCY,10
105
105
  ratio1/utils/config.py,sha256=Elfkl7W4aDMvB5WZLiYlPXrecBncgTxb4hcKhQedMzI,10111
106
106
  ratio1/utils/dotenv.py,sha256=_AgSo35n7EnQv5yDyu7C7i0kHragLJoCGydHjvOkrYY,2008
107
107
  ratio1/utils/oracle_sync/oracle_tester.py,sha256=aJOPcZhtbw1XPqsFG4qYpfv2Taj5-qRXbwJzrPyeXDE,27465
108
- ratio1-3.4.89.dist-info/METADATA,sha256=vBxFG7hy7e-OrtK-rXODKiollTOx0Zd_EOqIIF3zD5g,12255
109
- ratio1-3.4.89.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
110
- ratio1-3.4.89.dist-info/entry_points.txt,sha256=DR_olREzU1egwmgek3s4GfQslBi-KR7lXsd4ap0TFxE,46
111
- ratio1-3.4.89.dist-info/licenses/LICENSE,sha256=cvOsJVslde4oIaTCadabXnPqZmzcBO2f2zwXZRmJEbE,11311
112
- ratio1-3.4.89.dist-info/RECORD,,
108
+ ratio1-3.4.91.dist-info/METADATA,sha256=wGOM7ipeHJbAEwAbE72zSfkbbNlhdJwC_Xiv1p9jfmk,12255
109
+ ratio1-3.4.91.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
110
+ ratio1-3.4.91.dist-info/entry_points.txt,sha256=DR_olREzU1egwmgek3s4GfQslBi-KR7lXsd4ap0TFxE,46
111
+ ratio1-3.4.91.dist-info/licenses/LICENSE,sha256=cvOsJVslde4oIaTCadabXnPqZmzcBO2f2zwXZRmJEbE,11311
112
+ ratio1-3.4.91.dist-info/RECORD,,