Skip to content

Commit b84a43d

Browse files
authored
feat: Add FileInfoQuery (#181)
* feat: add FileInfo class Signed-off-by: dosi <[email protected]> * test: add FileInfo unit tests Signed-off-by: dosi <[email protected]> * feat: add FileInfoQuery Signed-off-by: dosi <[email protected]> * test: add FileInfoQuery unit tests Signed-off-by: dosi <[email protected]> * test: add integration tests for FileInfoQuery Signed-off-by: dosi <[email protected]> * docs: update examples README Signed-off-by: dosi <[email protected]> * docs: add file info query example Signed-off-by: dosi <[email protected]> * chore: add FileInfoQuery to __init__.py Signed-off-by: dosi <[email protected]> * chore: add Optional in set_file_id() Signed-off-by: dosi <[email protected]> * chore: add FileInfo to __init__.py Signed-off-by: dosi <[email protected]> * chore: add traceback.print_exec() to address PR feedback Signed-off-by: dosi <[email protected]> * chore: remove trailing Signed-off-by: dosi <[email protected]> * docs: update example to address PR feedback Signed-off-by: dosi <[email protected]> * test: update integration tests Signed-off-by: dosi <[email protected]> * test: add integration test for invalid file id Signed-off-by: dosi <[email protected]> * style: update linting and type hinting in file info query Signed-off-by: dosi <[email protected]> * style: update linting and type hinting in file info unit test Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent 83f42ed commit b84a43d

File tree

8 files changed

+790
-0
lines changed

8 files changed

+790
-0
lines changed

examples/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ You can choose either syntax or even mix both styles in your projects.
4848
- [Querying Topic Message](#querying-topic-message)
4949
- [File Transactions](#file-transactions)
5050
- [Creating a File](#creating-a-file)
51+
- [Querying File Info](#querying-file-info)
5152
- [Miscellaneous Queries](#miscellaneous-queries)
5253
- [Querying Transaction Record](#querying-transaction-record)
5354

@@ -939,6 +940,28 @@ transaction.execute(client)
939940
940941
transaction.execute(client)
941942
943+
```
944+
945+
### Querying File Info
946+
947+
#### Pythonic Syntax:
948+
```
949+
file_info_query = FileInfoQuery(file_id=file_id)
950+
file_info = file_info_query.execute(client)
951+
print(file_info)
952+
```
953+
954+
#### Method Chaining:
955+
```
956+
file_info = (
957+
FileInfoQuery()
958+
.set_file_id(file_id)
959+
.execute(client)
960+
)
961+
print(file_info)
962+
963+
```
964+
942965
## Miscellaneous Queries
943966

944967
### Querying Transaction Record

examples/query_file_info.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
This example demonstrates how to query file info using the Python SDK.
3+
"""
4+
5+
import os
6+
import sys
7+
8+
from dotenv import load_dotenv
9+
10+
from hiero_sdk_python import AccountId, Client, Network, PrivateKey
11+
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
12+
from hiero_sdk_python.file.file_info_query import FileInfoQuery
13+
from hiero_sdk_python.response_code import ResponseCode
14+
15+
load_dotenv()
16+
17+
18+
def setup_client():
19+
"""Initialize and set up the client with operator account"""
20+
network = Network(network="testnet")
21+
client = Client(network)
22+
23+
operator_id = AccountId.from_string(os.getenv("OPERATOR_ID"))
24+
operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY"))
25+
client.set_operator(operator_id, operator_key)
26+
27+
return client
28+
29+
30+
def create_file(client: Client):
31+
"""Create a test file"""
32+
file_private_key = PrivateKey.generate_ed25519()
33+
34+
receipt = (
35+
FileCreateTransaction()
36+
.set_keys([file_private_key.public_key(), client.operator_private_key.public_key()])
37+
.set_contents(b"Hello, this is a test file for querying!")
38+
.set_file_memo("Test file for query")
39+
.freeze_with(client)
40+
.sign(file_private_key)
41+
.execute(client)
42+
)
43+
44+
if receipt.status != ResponseCode.SUCCESS:
45+
print(f"File creation failed with status: {ResponseCode(receipt.status).name}")
46+
sys.exit(1)
47+
48+
file_id = receipt.file_id
49+
print(f"\nFile created with ID: {file_id}")
50+
51+
return file_id
52+
53+
54+
def query_file_info():
55+
"""
56+
Demonstrates querying file info by:
57+
1. Setting up client with operator account
58+
2. Creating a test file
59+
3. Querying the file info
60+
"""
61+
client = setup_client()
62+
63+
# Create a test file first
64+
file_id = create_file(client)
65+
66+
info = FileInfoQuery().set_file_id(file_id).execute(client)
67+
68+
print("\nFile Info:")
69+
print(f"File ID: {info.file_id}")
70+
print(f"Size: {info.size} bytes")
71+
print(f"Memo: {info.file_memo}")
72+
print(f"Expiration Time: {info.expiration_time}")
73+
print(f"Deleted: {info.is_deleted}")
74+
print(f"Total key(s): {len(info.keys)}")
75+
for i, key in enumerate(info.keys, 1):
76+
print(f"Key {i}: {key.to_string()}")
77+
78+
79+
if __name__ == "__main__":
80+
query_file_info()
81+

src/hiero_sdk_python/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@
8181

8282
# File
8383
from .file.file_create_transaction import FileCreateTransaction
84+
from .file.file_info_query import FileInfoQuery
85+
from .file.file_info import FileInfo
8486

8587
__all__ = [
8688
# Client
@@ -162,4 +164,6 @@
162164

163165
# File
164166
"FileCreateTransaction",
167+
"FileInfoQuery",
168+
"FileInfo",
165169
]
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from dataclasses import dataclass, field
2+
import datetime
3+
from typing import Optional
4+
from hiero_sdk_python.crypto.public_key import PublicKey
5+
from hiero_sdk_python.file.file_id import FileId
6+
from hiero_sdk_python.timestamp import Timestamp
7+
from hiero_sdk_python.hapi.services.file_get_info_pb2 import FileGetInfoResponse
8+
from hiero_sdk_python.hapi.services.basic_types_pb2 import KeyList as KeyListProto
9+
10+
@dataclass
11+
class FileInfo:
12+
"""
13+
Information about a file stored on the Hedera network.
14+
15+
Attributes:
16+
file_id (Optional[FileId]): The ID of the file
17+
size (Optional[int]): The size of the file in bytes
18+
expiration_time (Optional[Timestamp]): When the file will expire
19+
is_deleted (Optional[bool]): Whether the file has been deleted
20+
keys (list[PublicKey]): The keys that can modify this file
21+
file_memo (Optional[str]): The memo associated with the file
22+
ledger_id (Optional[bytes]): The ID of the ledger this file exists in
23+
"""
24+
file_id: Optional[FileId] = None
25+
size: Optional[int] = None
26+
expiration_time: Optional[Timestamp] = None
27+
is_deleted: Optional[bool] = None
28+
keys: list[PublicKey] = field(default_factory=list)
29+
file_memo: Optional[str] = None
30+
ledger_id: Optional[bytes] = None
31+
32+
@classmethod
33+
def _from_proto(cls, proto: FileGetInfoResponse.FileInfo) -> 'FileInfo':
34+
"""
35+
Creates a FileInfo instance from its protobuf representation.
36+
37+
Args:
38+
proto (FileGetInfoResponse.FileInfo): The protobuf to convert from.
39+
40+
Returns:
41+
FileInfo: A new FileInfo instance.
42+
"""
43+
if proto is None:
44+
raise ValueError("File info proto is None")
45+
46+
return cls(
47+
file_id=FileId._from_proto(proto.fileID),
48+
size=proto.size,
49+
expiration_time=Timestamp._from_protobuf(proto.expirationTime),
50+
is_deleted=proto.deleted,
51+
keys=[PublicKey._from_proto(key) for key in proto.keys.keys],
52+
file_memo=proto.memo,
53+
ledger_id=proto.ledger_id
54+
)
55+
56+
def _to_proto(self) -> FileGetInfoResponse.FileInfo:
57+
"""
58+
Converts this FileInfo instance to its protobuf representation.
59+
60+
Returns:
61+
FileGetInfoResponse.FileInfo: The protobuf representation of this FileInfo.
62+
"""
63+
return FileGetInfoResponse.FileInfo(
64+
fileID=self.file_id._to_proto() if self.file_id else None,
65+
size=self.size,
66+
expirationTime=self.expiration_time._to_protobuf() if self.expiration_time else None,
67+
deleted=self.is_deleted,
68+
keys=KeyListProto(keys=[key._to_proto() for key in self.keys or []]),
69+
memo=self.file_memo,
70+
ledger_id=self.ledger_id
71+
)
72+
73+
def __repr__(self) -> str:
74+
"""
75+
Returns a string representation of the FileInfo object.
76+
77+
Returns:
78+
str: A string representation of the FileInfo object.
79+
"""
80+
return self.__str__()
81+
82+
def __str__(self) -> str:
83+
"""
84+
Pretty-print the FileInfo.
85+
"""
86+
# Format expiration time as datetime if available
87+
exp_dt = (
88+
datetime.datetime.fromtimestamp(self.expiration_time.seconds)
89+
if self.expiration_time and hasattr(self.expiration_time, "seconds")
90+
else self.expiration_time
91+
)
92+
93+
# Format keys as readable strings
94+
keys_str = [key.to_string() for key in self.keys] if self.keys else []
95+
96+
# Format ledger_id as hex if it's bytes
97+
ledger_id_display = (
98+
f"0x{self.ledger_id.hex()}"
99+
if isinstance(self.ledger_id, (bytes, bytearray))
100+
else self.ledger_id
101+
)
102+
103+
return (
104+
"FileInfo(\n"
105+
f" file_id={self.file_id},\n"
106+
f" size={self.size},\n"
107+
f" expiration_time={exp_dt},\n"
108+
f" is_deleted={self.is_deleted},\n"
109+
f" keys={keys_str},\n"
110+
f" file_memo='{self.file_memo}',\n"
111+
f" ledger_id={ledger_id_display}\n"
112+
")"
113+
)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
Query to get information about a file on the network.
3+
"""
4+
5+
import traceback
6+
from typing import Optional
7+
8+
from hiero_sdk_python.channels import _Channel
9+
from hiero_sdk_python.client.client import Client
10+
from hiero_sdk_python.executable import _Method
11+
from hiero_sdk_python.file.file_id import FileId
12+
from hiero_sdk_python.file.file_info import FileInfo
13+
from hiero_sdk_python.hapi.services import file_get_info_pb2, query_pb2, response_pb2
14+
from hiero_sdk_python.hapi.services.file_get_info_pb2 import FileGetInfoResponse
15+
from hiero_sdk_python.query.query import Query
16+
17+
18+
class FileInfoQuery(Query):
19+
"""
20+
A query to retrieve information about a specific File.
21+
22+
This class constructs and executes a query to retrieve information
23+
about a file on the network, including the file's properties and settings.
24+
25+
"""
26+
27+
def __init__(self, file_id: Optional[FileId] = None) -> None:
28+
"""
29+
Initializes a new FileInfoQuery instance with an optional file_id.
30+
31+
Args:
32+
file_id (Optional[FileId], optional): The ID of the file to query.
33+
"""
34+
super().__init__()
35+
self.file_id: Optional[FileId] = file_id
36+
37+
def set_file_id(self, file_id: Optional[FileId]) -> "FileInfoQuery":
38+
"""
39+
Sets the ID of the file to query.
40+
41+
Args:
42+
file_id (Optional[FileId]): The ID of the file.
43+
44+
Returns:
45+
FileInfoQuery: Returns self for method chaining.
46+
"""
47+
self.file_id = file_id
48+
return self
49+
50+
def _make_request(self) -> query_pb2.Query:
51+
"""
52+
Constructs the protobuf request for the query.
53+
54+
Builds a FileGetInfoQuery protobuf message with the
55+
appropriate header and file ID.
56+
57+
Returns:
58+
Query: The protobuf query message.
59+
60+
Raises:
61+
ValueError: If the file ID is not set.
62+
Exception: If any other error occurs during request construction.
63+
"""
64+
try:
65+
if not self.file_id:
66+
raise ValueError("File ID must be set before making the request.")
67+
68+
query_header = self._make_request_header()
69+
70+
file_info_query = file_get_info_pb2.FileGetInfoQuery()
71+
file_info_query.header.CopyFrom(query_header)
72+
file_info_query.fileID.CopyFrom(self.file_id._to_proto())
73+
74+
query = query_pb2.Query()
75+
query.fileGetInfo.CopyFrom(file_info_query)
76+
77+
return query
78+
except Exception as e:
79+
print(f"Exception in _make_request: {e}")
80+
traceback.print_exc()
81+
raise
82+
83+
def _get_method(self, channel: _Channel) -> _Method:
84+
"""
85+
Returns the appropriate gRPC method for the file info query.
86+
87+
Implements the abstract method from Query to provide the specific
88+
gRPC method for getting file information.
89+
90+
Args:
91+
channel (_Channel): The channel containing service stubs
92+
93+
Returns:
94+
_Method: The method wrapper containing the query function
95+
"""
96+
return _Method(transaction_func=None, query_func=channel.file.getFileInfo)
97+
98+
def execute(self, client: Client) -> FileInfo:
99+
"""
100+
Executes the file info query.
101+
102+
Sends the query to the Hedera network and processes the response
103+
to return a FileInfo object.
104+
105+
This function delegates the core logic to `_execute()`, and may propagate
106+
exceptions raised by it.
107+
108+
Args:
109+
client (Client): The client instance to use for execution
110+
111+
Returns:
112+
FileInfo: The file info from the network
113+
114+
Raises:
115+
PrecheckError: If the query fails with a non-retryable error
116+
MaxAttemptsError: If the query fails after the maximum number of attempts
117+
ReceiptStatusError: If the query fails with a receipt status error
118+
"""
119+
self._before_execute(client)
120+
response = self._execute(client)
121+
122+
return FileInfo._from_proto(response.fileGetInfo.fileInfo)
123+
124+
def _get_query_response(
125+
self, response: response_pb2.Response
126+
) -> FileGetInfoResponse.FileInfo:
127+
"""
128+
Extracts the file info response from the full response.
129+
130+
Implements the abstract method from Query to extract the
131+
specific file info response object.
132+
133+
Args:
134+
response: The full response from the network
135+
136+
Returns:
137+
The file get info response object
138+
"""
139+
return response.fileGetInfo

0 commit comments

Comments
 (0)