hippius 0.1.9__py3-none-any.whl → 0.1.10__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.
- {hippius-0.1.9.dist-info → hippius-0.1.10.dist-info}/METADATA +93 -1
- hippius-0.1.10.dist-info/RECORD +12 -0
- hippius_sdk/__init__.py +4 -0
- hippius_sdk/account.py +648 -0
- hippius_sdk/cli.py +643 -443
- hippius_sdk/config.py +71 -0
- hippius_sdk/substrate.py +202 -134
- hippius_sdk/utils.py +87 -0
- hippius-0.1.9.dist-info/RECORD +0 -10
- {hippius-0.1.9.dist-info → hippius-0.1.10.dist-info}/WHEEL +0 -0
- {hippius-0.1.9.dist-info → hippius-0.1.10.dist-info}/entry_points.txt +0 -0
hippius_sdk/config.py
CHANGED
@@ -740,5 +740,76 @@ def reset_config() -> bool:
|
|
740
740
|
return save_config(DEFAULT_CONFIG.copy())
|
741
741
|
|
742
742
|
|
743
|
+
def get_keypair(
|
744
|
+
ss58_address: Optional[str] = None, account_name: Optional[str] = None
|
745
|
+
) -> "Keypair":
|
746
|
+
"""
|
747
|
+
Get a Keypair object for a given SS58 address or account name.
|
748
|
+
|
749
|
+
This function will attempt to find the specified account and generate
|
750
|
+
a Keypair object using the stored seed phrase.
|
751
|
+
|
752
|
+
Args:
|
753
|
+
ss58_address: SS58 address of the account
|
754
|
+
account_name: Name of the account (used if ss58_address is None)
|
755
|
+
|
756
|
+
Returns:
|
757
|
+
Keypair: A substrate Keypair object
|
758
|
+
|
759
|
+
Raises:
|
760
|
+
ValueError: If the account cannot be found or if the seed phrase is not available
|
761
|
+
ImportError: If the required dependencies are not installed
|
762
|
+
"""
|
763
|
+
# Import here to avoid circular imports
|
764
|
+
try:
|
765
|
+
from substrateinterface import Keypair
|
766
|
+
except ImportError:
|
767
|
+
raise ImportError(
|
768
|
+
"Substrate interface is required to get a keypair. "
|
769
|
+
"Install with: pip install substrate-interface"
|
770
|
+
)
|
771
|
+
|
772
|
+
# If ss58_address is provided, look for a matching account
|
773
|
+
if ss58_address:
|
774
|
+
accounts = list_accounts()
|
775
|
+
found_account = None
|
776
|
+
|
777
|
+
for name, data in accounts.items():
|
778
|
+
if data.get("ss58_address") == ss58_address:
|
779
|
+
found_account = name
|
780
|
+
break
|
781
|
+
|
782
|
+
if found_account:
|
783
|
+
account_name = found_account
|
784
|
+
else:
|
785
|
+
raise ValueError(f"No account found with SS58 address: {ss58_address}")
|
786
|
+
|
787
|
+
# If no account_name by this point, use the active account
|
788
|
+
if not account_name:
|
789
|
+
account_name = get_active_account()
|
790
|
+
if not account_name:
|
791
|
+
raise ValueError(
|
792
|
+
"No account specified and no active account. "
|
793
|
+
"Set an active account with: hippius account switch <account_name>"
|
794
|
+
)
|
795
|
+
|
796
|
+
# Get the seed phrase for the account
|
797
|
+
seed_phrase = get_seed_phrase(account_name=account_name)
|
798
|
+
if not seed_phrase:
|
799
|
+
if get_config_value("substrate", "seed_phrase_encoded"):
|
800
|
+
raise ValueError(
|
801
|
+
f"The seed phrase for account '{account_name}' is encrypted. "
|
802
|
+
f"Please decrypt it first with: hippius seed decode --account {account_name}"
|
803
|
+
)
|
804
|
+
else:
|
805
|
+
raise ValueError(
|
806
|
+
f"No seed phrase found for account '{account_name}'. "
|
807
|
+
f'Set one with: hippius seed set "your seed phrase" --account {account_name}'
|
808
|
+
)
|
809
|
+
|
810
|
+
# Create and return the keypair
|
811
|
+
return Keypair.create_from_mnemonic(seed_phrase)
|
812
|
+
|
813
|
+
|
743
814
|
# Initialize configuration on module import
|
744
815
|
ensure_config_dir()
|
hippius_sdk/substrate.py
CHANGED
@@ -599,6 +599,29 @@ class SubstrateClient:
|
|
599
599
|
ConnectionError: If connection to Substrate fails
|
600
600
|
ValueError: If query fails
|
601
601
|
"""
|
602
|
+
# For backward compatibility, this method now calls get_user_files_from_profile
|
603
|
+
# with appropriate conversions
|
604
|
+
return self.get_user_files_from_profile(account_address)
|
605
|
+
|
606
|
+
def get_user_files_from_profile(
|
607
|
+
self,
|
608
|
+
account_address: Optional[str] = None,
|
609
|
+
) -> List[Dict[str, Any]]:
|
610
|
+
"""
|
611
|
+
Get user files by fetching the user profile CID from ipfsPallet and then retrieving
|
612
|
+
the profile JSON from IPFS.
|
613
|
+
|
614
|
+
Args:
|
615
|
+
account_address: Substrate account address (uses keypair address if not specified)
|
616
|
+
Format: 5H1QBRF7T7dgKwzVGCgS4wioudvMRf9K4NEDzfuKLnuyBNzH
|
617
|
+
|
618
|
+
Returns:
|
619
|
+
List[Dict[str, Any]]: List of file objects from the user profile
|
620
|
+
|
621
|
+
Raises:
|
622
|
+
ConnectionError: If connection to Substrate fails
|
623
|
+
ValueError: If query fails or profile cannot be retrieved
|
624
|
+
"""
|
602
625
|
try:
|
603
626
|
# Initialize Substrate connection if not already connected
|
604
627
|
if not hasattr(self, "_substrate") or self._substrate is None:
|
@@ -622,159 +645,204 @@ class SubstrateClient:
|
|
622
645
|
account_address = self._keypair.ss58_address
|
623
646
|
print(f"Using keypair address: {account_address}")
|
624
647
|
|
625
|
-
#
|
626
|
-
|
627
|
-
|
628
|
-
"
|
629
|
-
"
|
630
|
-
|
631
|
-
|
648
|
+
# Query the blockchain for the user profile CID
|
649
|
+
print(f"Querying user profile for account: {account_address}")
|
650
|
+
result = self._substrate.query(
|
651
|
+
module="IpfsPallet",
|
652
|
+
storage_function="UserProfile",
|
653
|
+
params=[account_address],
|
654
|
+
)
|
632
655
|
|
633
|
-
|
656
|
+
# Check if a profile was found
|
657
|
+
if not result.value:
|
658
|
+
print(f"No profile found for account: {account_address}")
|
659
|
+
return []
|
634
660
|
|
635
|
-
#
|
636
|
-
|
637
|
-
|
638
|
-
|
661
|
+
# The result is a hex-encoded IPFS CID
|
662
|
+
# Handle both cases: bytes (needs .hex()) and string (already hex)
|
663
|
+
if isinstance(result.value, bytes):
|
664
|
+
hex_cid = result.value.hex()
|
665
|
+
else:
|
666
|
+
# If it's already a string, use it directly
|
667
|
+
hex_cid = result.value
|
639
668
|
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
#
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
669
|
+
# Remove '0x' prefix if present
|
670
|
+
if hex_cid.startswith("0x"):
|
671
|
+
hex_cid = hex_cid[2:]
|
672
|
+
|
673
|
+
print(f"Found user profile CID (hex): {hex_cid}")
|
674
|
+
|
675
|
+
# Convert the hex CID to a readable IPFS CID
|
676
|
+
profile_cid = self._hex_to_ipfs_cid(hex_cid)
|
677
|
+
print(f"Decoded IPFS CID: {profile_cid}")
|
678
|
+
|
679
|
+
# Fetch the profile JSON from IPFS
|
680
|
+
from hippius_sdk.ipfs import IPFSClient
|
681
|
+
|
682
|
+
ipfs_client = IPFSClient()
|
683
|
+
|
684
|
+
print(f"Fetching user profile from IPFS: {profile_cid}")
|
685
|
+
profile_data = ipfs_client.cat(profile_cid)
|
686
|
+
|
687
|
+
# Parse the JSON content
|
688
|
+
if not profile_data.get("is_text", False):
|
689
|
+
raise ValueError("User profile is not in text format")
|
690
|
+
|
691
|
+
profile_json = json.loads(profile_data.get("content", "{}"))
|
692
|
+
print(f"Successfully retrieved user profile")
|
693
|
+
|
694
|
+
# Extract the file list from the profile
|
695
|
+
# The profile might be either a dictionary with a 'files' key or a direct list of files
|
696
|
+
files = []
|
697
|
+
if isinstance(profile_json, dict):
|
698
|
+
files = profile_json.get("files", [])
|
699
|
+
elif isinstance(profile_json, list):
|
700
|
+
# The profile itself might be a list of files
|
701
|
+
files = profile_json
|
702
|
+
else:
|
703
|
+
print(f"Warning: Unexpected profile structure: {type(profile_json)}")
|
704
|
+
|
705
|
+
print(f"Found {len(files)} files in user profile")
|
706
|
+
|
707
|
+
# Process the files to match the expected format
|
708
|
+
processed_files = []
|
709
|
+
for file in files:
|
710
|
+
# Make sure file is a dictionary
|
711
|
+
if not isinstance(file, dict):
|
712
|
+
# Skip non-dictionary entries silently
|
713
|
+
continue
|
714
|
+
|
715
|
+
# Convert numeric arrays to strings if needed
|
716
|
+
# Handle file_hash: could be an array of ASCII/UTF-8 code points
|
717
|
+
file_hash = None
|
718
|
+
raw_file_hash = file.get("file_hash")
|
719
|
+
if isinstance(raw_file_hash, list) and all(
|
720
|
+
isinstance(n, int) for n in raw_file_hash
|
721
|
+
):
|
667
722
|
try:
|
668
|
-
#
|
669
|
-
|
670
|
-
hex_bytes = bytes.fromhex(cid_str)
|
671
|
-
ascii_str = hex_bytes.decode("ascii")
|
672
|
-
|
673
|
-
# If the decoded string starts with a valid CID prefix, return it
|
674
|
-
if ascii_str.startswith(
|
675
|
-
("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")
|
676
|
-
):
|
677
|
-
return ascii_str
|
723
|
+
# Convert array of numbers to bytes, then to a string
|
724
|
+
file_hash = bytes(raw_file_hash).decode("utf-8")
|
678
725
|
except Exception:
|
679
726
|
pass
|
727
|
+
else:
|
728
|
+
# Try different field names for the CID that might be in the profile
|
729
|
+
file_hash = (
|
730
|
+
file.get("cid")
|
731
|
+
or file.get("hash")
|
732
|
+
or file.get("fileHash")
|
733
|
+
or raw_file_hash
|
734
|
+
)
|
680
735
|
|
681
|
-
|
736
|
+
# Handle file_name: could be an array of ASCII/UTF-8 code points
|
737
|
+
file_name = None
|
738
|
+
raw_file_name = file.get("file_name")
|
739
|
+
if isinstance(raw_file_name, list) and all(
|
740
|
+
isinstance(n, int) for n in raw_file_name
|
741
|
+
):
|
682
742
|
try:
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
try:
|
688
|
-
binary_data = binascii.unhexlify(cid_str)
|
689
|
-
if (
|
690
|
-
len(binary_data) > 2
|
691
|
-
and binary_data[0] == 0x12
|
692
|
-
and binary_data[1] == 0x20
|
693
|
-
):
|
694
|
-
# This looks like a CIDv0 (Qm...)
|
695
|
-
decoded_cid = base58.b58encode(binary_data).decode(
|
696
|
-
"utf-8"
|
697
|
-
)
|
698
|
-
return decoded_cid
|
699
|
-
except Exception:
|
700
|
-
pass
|
701
|
-
|
702
|
-
# If not successful, just return hex with 0x prefix as fallback
|
703
|
-
return f"0x{cid_str}"
|
704
|
-
except ImportError:
|
705
|
-
# If base58 is not available, return hex with prefix
|
706
|
-
return f"0x{cid_str}"
|
707
|
-
|
708
|
-
# Default case - return as is
|
709
|
-
return cid_str
|
710
|
-
|
711
|
-
# Helper function to format file sizes
|
712
|
-
def format_file_size(size_bytes):
|
713
|
-
if size_bytes >= 1024 * 1024:
|
714
|
-
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
743
|
+
# Convert array of numbers to bytes, then to a string
|
744
|
+
file_name = bytes(raw_file_name).decode("utf-8")
|
745
|
+
except Exception:
|
746
|
+
pass
|
715
747
|
else:
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
and miner_id.startswith("1")
|
724
|
-
and len(miner_id) > 40
|
725
|
-
):
|
726
|
-
# Truncate long peer IDs
|
727
|
-
return f"{miner_id[:12]}...{miner_id[-4:]}"
|
728
|
-
return miner_id
|
729
|
-
|
730
|
-
# Process the response
|
731
|
-
processed_files = []
|
732
|
-
for file in files:
|
733
|
-
processed_file = {"file_size": file.get("file_size", 0)}
|
748
|
+
# Try different field names for the filename
|
749
|
+
file_name = (
|
750
|
+
file.get("filename")
|
751
|
+
or file.get("name")
|
752
|
+
or file.get("fileName")
|
753
|
+
or raw_file_name
|
754
|
+
)
|
734
755
|
|
735
|
-
#
|
736
|
-
|
737
|
-
|
756
|
+
# Try different field names for the size
|
757
|
+
file_size = (
|
758
|
+
file.get("size")
|
759
|
+
or file.get("fileSize")
|
760
|
+
or file.get("file_size")
|
761
|
+
or 0
|
738
762
|
)
|
739
763
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
if "miner_ids" in file and isinstance(file["miner_ids"], list):
|
751
|
-
all_miners = [
|
752
|
-
ascii_to_string(miner_id) for miner_id in file["miner_ids"]
|
753
|
-
]
|
754
|
-
processed_file["miner_ids_full"] = all_miners
|
755
|
-
processed_file["miner_count"] = len(all_miners)
|
756
|
-
|
757
|
-
# Truncate miner list if requested
|
758
|
-
if max_miners > 0 and len(all_miners) > max_miners:
|
759
|
-
displayed_miners = all_miners[:max_miners]
|
760
|
-
else:
|
761
|
-
displayed_miners = all_miners
|
764
|
+
processed_file = {
|
765
|
+
"file_hash": file_hash,
|
766
|
+
"file_name": file_name,
|
767
|
+
# Add any other fields available in the profile
|
768
|
+
"miner_ids": file.get(
|
769
|
+
"miner_ids", []
|
770
|
+
), # Try to get miners if available
|
771
|
+
"miner_count": len(file.get("miner_ids", [])), # Count the miners
|
772
|
+
"file_size": file_size,
|
773
|
+
}
|
762
774
|
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
775
|
+
# Add formatted file size if available
|
776
|
+
if file_size:
|
777
|
+
size_bytes = file_size
|
778
|
+
if size_bytes >= 1024 * 1024:
|
779
|
+
processed_file[
|
780
|
+
"size_formatted"
|
781
|
+
] = f"{size_bytes / (1024 * 1024):.2f} MB"
|
782
|
+
else:
|
783
|
+
processed_file["size_formatted"] = f"{size_bytes / 1024:.2f} KB"
|
768
784
|
else:
|
769
|
-
processed_file["
|
770
|
-
processed_file["miner_ids_full"] = []
|
771
|
-
processed_file["miner_count"] = 0
|
785
|
+
processed_file["size_formatted"] = "Unknown"
|
772
786
|
|
773
787
|
processed_files.append(processed_file)
|
774
788
|
|
775
789
|
return processed_files
|
776
790
|
|
777
791
|
except Exception as e:
|
778
|
-
error_msg = f"Error
|
792
|
+
error_msg = f"Error retrieving user files from profile: {str(e)}"
|
779
793
|
print(error_msg)
|
780
794
|
raise ValueError(error_msg)
|
795
|
+
|
796
|
+
def _hex_to_ipfs_cid(self, hex_string: str) -> str:
|
797
|
+
"""
|
798
|
+
Convert a hex-encoded IPFS CID to a regular IPFS CID.
|
799
|
+
|
800
|
+
Args:
|
801
|
+
hex_string: Hex string representation of an IPFS CID
|
802
|
+
|
803
|
+
Returns:
|
804
|
+
str: Regular IPFS CID
|
805
|
+
"""
|
806
|
+
# First, try to decode as ASCII if it's a hex representation of ASCII characters
|
807
|
+
try:
|
808
|
+
if hex_string.startswith("0x"):
|
809
|
+
hex_string = hex_string[2:]
|
810
|
+
|
811
|
+
bytes_data = bytes.fromhex(hex_string)
|
812
|
+
ascii_str = bytes_data.decode("ascii")
|
813
|
+
|
814
|
+
# If the decoded string starts with a valid CID prefix, return it
|
815
|
+
if ascii_str.startswith(("Qm", "bafy", "bafk", "bafyb", "bafzb", "b")):
|
816
|
+
return ascii_str
|
817
|
+
except Exception:
|
818
|
+
# If ASCII decoding fails, continue with other methods
|
819
|
+
pass
|
820
|
+
|
821
|
+
# Try to decode as a binary CID
|
822
|
+
try:
|
823
|
+
import base58
|
824
|
+
|
825
|
+
if hex_string.startswith("0x"):
|
826
|
+
hex_string = hex_string[2:]
|
827
|
+
|
828
|
+
binary_data = bytes.fromhex(hex_string)
|
829
|
+
|
830
|
+
# Check if it matches CIDv0 pattern (starts with 0x12, 0x20)
|
831
|
+
if (
|
832
|
+
len(binary_data) > 2
|
833
|
+
and binary_data[0] == 0x12
|
834
|
+
and binary_data[1] == 0x20
|
835
|
+
):
|
836
|
+
# CIDv0 (Qm...)
|
837
|
+
return base58.b58encode(binary_data).decode("utf-8")
|
838
|
+
|
839
|
+
# If it doesn't match CIDv0, for CIDv1 just return the hex without 0x prefix
|
840
|
+
# since adding 0x breaks IPFS gateway URLs
|
841
|
+
return hex_string
|
842
|
+
except ImportError:
|
843
|
+
# If base58 is not available
|
844
|
+
print("Warning: base58 module not available for proper CID conversion")
|
845
|
+
return hex_string
|
846
|
+
except Exception as e:
|
847
|
+
print(f"Error converting hex to CID: {e}")
|
848
|
+
return hex_string
|
hippius_sdk/utils.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for the Hippius SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import math
|
7
|
+
from typing import Dict, Any, Union, List, Optional
|
8
|
+
|
9
|
+
|
10
|
+
def ensure_directory_exists(directory_path: str) -> None:
|
11
|
+
"""
|
12
|
+
Create a directory if it doesn't exist.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
directory_path: Path to the directory to ensure exists
|
16
|
+
"""
|
17
|
+
if not os.path.exists(directory_path):
|
18
|
+
os.makedirs(directory_path, exist_ok=True)
|
19
|
+
|
20
|
+
|
21
|
+
def format_size(size_bytes: int) -> str:
|
22
|
+
"""
|
23
|
+
Format a size in bytes to a human-readable string.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
size_bytes: Size in bytes
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
str: Human-readable size (e.g., "1.23 MB")
|
30
|
+
"""
|
31
|
+
if size_bytes == 0:
|
32
|
+
return "0 B"
|
33
|
+
|
34
|
+
size_names = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
35
|
+
i = int(math.floor(math.log(size_bytes, 1024)))
|
36
|
+
if i >= len(size_names):
|
37
|
+
i = len(size_names) - 1
|
38
|
+
p = math.pow(1024, i)
|
39
|
+
s = round(size_bytes / p, 2)
|
40
|
+
return f"{s} {size_names[i]}"
|
41
|
+
|
42
|
+
|
43
|
+
def deep_merge(target: Dict[str, Any], source: Dict[str, Any]) -> Dict[str, Any]:
|
44
|
+
"""
|
45
|
+
Deep merge two dictionaries, with source taking precedence over target.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
target: Target dictionary to merge into
|
49
|
+
source: Source dictionary to merge from
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Dict[str, Any]: Merged dictionary
|
53
|
+
"""
|
54
|
+
for key, value in source.items():
|
55
|
+
if key in target and isinstance(target[key], dict) and isinstance(value, dict):
|
56
|
+
target[key] = deep_merge(target[key], value)
|
57
|
+
else:
|
58
|
+
target[key] = value
|
59
|
+
return target
|
60
|
+
|
61
|
+
|
62
|
+
def parse_comma_separated(value: Optional[str]) -> List[str]:
|
63
|
+
"""
|
64
|
+
Parse a comma-separated string into a list.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
value: Comma-separated string or None
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
List[str]: List of stripped values, or empty list if value is None
|
71
|
+
"""
|
72
|
+
if not value:
|
73
|
+
return []
|
74
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
75
|
+
|
76
|
+
|
77
|
+
def is_valid_url(url: str) -> bool:
|
78
|
+
"""
|
79
|
+
Basic check if a string is a valid URL.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
url: URL to check
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
bool: True if valid URL, False otherwise
|
86
|
+
"""
|
87
|
+
return url.startswith(("http://", "https://", "ws://", "wss://"))
|
hippius-0.1.9.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
hippius_sdk/__init__.py,sha256=fs2pZ_wIl3eaUz6qFprcRsZhYakqv6-qm468NZh-8h4,1260
|
2
|
-
hippius_sdk/cli.py,sha256=0IQZh2by6kN8Sol3eDFfZzgj5otBeIOKC4lq2wIejOc,76317
|
3
|
-
hippius_sdk/client.py,sha256=54tsg4k29sqt3F77LQJ_vhzzTR73QuZ_edqI_BvZM1E,14905
|
4
|
-
hippius_sdk/config.py,sha256=gTr8EXeZ3jJNchcG9WyjbQdpPRHTxL5IWZgyIAm_X-c,22869
|
5
|
-
hippius_sdk/ipfs.py,sha256=9fds5MJwVb7t8IqROM70x9fWgyk9_Ot5psat_hMnRN8,63969
|
6
|
-
hippius_sdk/substrate.py,sha256=fa8K8vVdYGVtB3VNJ-_Vdw8bOxDUajFmj6se-rHZveQ,30853
|
7
|
-
hippius-0.1.9.dist-info/METADATA,sha256=SPXMFKAJ_at35FEPB9uDMi3iBP7EXMLxskPvLxHmCQE,28000
|
8
|
-
hippius-0.1.9.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
9
|
-
hippius-0.1.9.dist-info/entry_points.txt,sha256=b1lo60zRXmv1ud-c5BC-cJcAfGE5FD4qM_nia6XeQtM,98
|
10
|
-
hippius-0.1.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|