Skip to content

Commit 7e9e417

Browse files
authored
feat: add FileContentsQuery (#217)
* feat: add FileContentsQuery Signed-off-by: dosi <[email protected]> * feat: add integration tests for FileContentsQuery Signed-off-by: dosi <[email protected]> * test: add unit tests for FileContentsQuery Signed-off-by: dosi <[email protected]> * docs: add file contents query example Signed-off-by: dosi <[email protected]> * docs: add FileContentsQuery to examples README Signed-off-by: dosi <[email protected]> * chore: add FileContentsQuery to __init__.py Signed-off-by: dosi <[email protected]> --------- Signed-off-by: dosi <[email protected]>
1 parent c3558cb commit 7e9e417

File tree

6 files changed

+460
-0
lines changed

6 files changed

+460
-0
lines changed

examples/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ You can choose either syntax or even mix both styles in your projects.
4949
- [File Transactions](#file-transactions)
5050
- [Creating a File](#creating-a-file)
5151
- [Querying File Info](#querying-file-info)
52+
- [Querying File Contents](#querying-file-contents)
5253
- [Deleting a File](#deleting-a-file)
5354
- [Miscellaneous Queries](#miscellaneous-queries)
5455
- [Querying Transaction Record](#querying-transaction-record)
@@ -963,6 +964,26 @@ print(file_info)
963964
964965
```
965966

967+
### Querying File Contents
968+
969+
#### Pythonic Syntax:
970+
```
971+
file_contents_query = FileContentsQuery(file_id=file_id)
972+
file_contents = file_contents_query.execute(client)
973+
print(str(file_contents)) # decode bytes to string
974+
```
975+
976+
#### Method Chaining:
977+
```
978+
file_contents = (
979+
FileContentsQuery()
980+
.set_file_id(file_id)
981+
.execute(client)
982+
)
983+
print(str(file_contents)) # decode bytes to string
984+
985+
```
986+
966987
### Deleting a File
967988

968989
#### Pythonic Syntax:

examples/query_file_contents.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
This example demonstrates how to query file contents 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_contents_query import FileContentsQuery
12+
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
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"Test contents to be queried!")
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_contents():
55+
"""
56+
Demonstrates querying file contents by:
57+
1. Setting up client with operator account
58+
2. Creating a test file
59+
3. Querying the file contents
60+
"""
61+
client = setup_client()
62+
63+
# Create a test file first
64+
file_id = create_file(client)
65+
66+
contents = FileContentsQuery().set_file_id(file_id).execute(client)
67+
68+
# The contets are returned as bytes and we need to decode them
69+
print("\nFile Contents:")
70+
print(str(contents))
71+
72+
73+
if __name__ == "__main__":
74+
query_file_contents()

src/hiero_sdk_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
from .file.file_create_transaction import FileCreateTransaction
8484
from .file.file_info_query import FileInfoQuery
8585
from .file.file_info import FileInfo
86+
from .file.file_contents_query import FileContentsQuery
8687
from .file.file_delete_transaction import FileDeleteTransaction
8788

8889
__all__ = [
@@ -167,5 +168,6 @@
167168
"FileCreateTransaction",
168169
"FileInfoQuery",
169170
"FileInfo",
171+
"FileContentsQuery",
170172
"FileDeleteTransaction",
171173
]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
Query to get the contents of a file on the network.
3+
"""
4+
5+
from typing import Optional
6+
7+
from hiero_sdk_python.channels import _Channel
8+
from hiero_sdk_python.client.client import Client
9+
from hiero_sdk_python.executable import _Method
10+
from hiero_sdk_python.file.file_id import FileId
11+
from hiero_sdk_python.hapi.services import (
12+
file_get_contents_pb2,
13+
query_pb2,
14+
response_pb2,
15+
)
16+
from hiero_sdk_python.hapi.services.file_get_contents_pb2 import FileGetContentsResponse
17+
from hiero_sdk_python.query.query import Query
18+
19+
20+
class FileContentsQuery(Query):
21+
"""
22+
A query to retrieve the contents of a specific File.
23+
24+
This class constructs and executes a query to retrieve the contents
25+
of a file on the network.
26+
27+
"""
28+
29+
def __init__(self, file_id: Optional[FileId] = None) -> None:
30+
"""
31+
Initializes a new FileContentsQuery instance with an optional file_id.
32+
33+
Args:
34+
file_id (Optional[FileId], optional): The ID of the file to query.
35+
"""
36+
super().__init__()
37+
self.file_id = file_id
38+
39+
def set_file_id(self, file_id: Optional[FileId]) -> "FileContentsQuery":
40+
"""
41+
Sets the ID of the file to query.
42+
43+
Args:
44+
file_id (Optional[FileId]): The ID of the file.
45+
46+
Returns:
47+
FileContentsQuery: Returns self for method chaining.
48+
"""
49+
self.file_id = file_id
50+
return self
51+
52+
def _make_request(self) -> query_pb2.Query:
53+
"""
54+
Constructs the protobuf request for the query.
55+
56+
Builds a FileGetContentsQuery protobuf message with the
57+
appropriate header and file ID.
58+
59+
Returns:
60+
Query: The protobuf query message.
61+
62+
Raises:
63+
ValueError: If the file ID is not set.
64+
Exception: If any other error occurs during request construction.
65+
"""
66+
try:
67+
if not self.file_id:
68+
raise ValueError("File ID must be set before making the request.")
69+
70+
query_header = self._make_request_header()
71+
72+
file_contents_query = file_get_contents_pb2.FileGetContentsQuery()
73+
file_contents_query.header.CopyFrom(query_header)
74+
file_contents_query.fileID.CopyFrom(self.file_id._to_proto())
75+
76+
query = query_pb2.Query()
77+
query.fileGetContents.CopyFrom(file_contents_query)
78+
79+
return query
80+
except Exception as e:
81+
print(f"Exception in _make_request: {e}")
82+
raise
83+
84+
def _get_method(self, channel: _Channel) -> _Method:
85+
"""
86+
Returns the appropriate gRPC method for the file contents query.
87+
88+
Implements the abstract method from Query to provide the specific
89+
gRPC method for getting file contents.
90+
91+
Args:
92+
channel (_Channel): The channel containing service stubs
93+
94+
Returns:
95+
_Method: The method wrapper containing the query function
96+
"""
97+
return _Method(transaction_func=None, query_func=channel.file.getFileContent)
98+
99+
def execute(self, client: Client) -> str:
100+
"""
101+
Executes the file contents query.
102+
103+
Sends the query to the Hedera network and processes the response
104+
to return the file contents.
105+
106+
This function delegates the core logic to `_execute()`, and may propagate
107+
exceptions raised by it.
108+
109+
Args:
110+
client (Client): The client instance to use for execution
111+
112+
Returns:
113+
str: The contents of the file from the network
114+
115+
Raises:
116+
PrecheckError: If the query fails with a non-retryable error
117+
MaxAttemptsError: If the query fails after the maximum number of attempts
118+
ReceiptStatusError: If the query fails with a receipt status error
119+
"""
120+
self._before_execute(client)
121+
response = self._execute(client)
122+
123+
return response.fileGetContents.fileContents.contents
124+
125+
def _get_query_response(
126+
self, response: response_pb2.Response
127+
) -> FileGetContentsResponse:
128+
"""
129+
Extracts the file contents response from the full response.
130+
131+
Implements the abstract method from Query to extract the
132+
specific file contents response object.
133+
134+
Args:
135+
response: The full response from the network
136+
137+
Returns:
138+
The file get contents response object
139+
"""
140+
return response.fileGetContents
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Integration tests for FileContentsQuery.
3+
"""
4+
5+
import pytest
6+
7+
from hiero_sdk_python.exceptions import PrecheckError
8+
from hiero_sdk_python.file.file_contents_query import FileContentsQuery
9+
from hiero_sdk_python.file.file_create_transaction import FileCreateTransaction
10+
from hiero_sdk_python.file.file_id import FileId
11+
from hiero_sdk_python.hbar import Hbar
12+
from tests.integration.utils_for_test import env
13+
14+
FILE_CONTENT = b"Hello, World"
15+
16+
17+
@pytest.mark.integration
18+
def test_integration_file_contents_query_can_execute(env):
19+
"""Test that the FileContentsQuery can be executed successfully."""
20+
# Create a file
21+
receipt = (
22+
FileCreateTransaction()
23+
.set_keys([env.operator_key.public_key()])
24+
.set_contents(FILE_CONTENT)
25+
.set_transaction_memo("python sdk e2e tests")
26+
.execute(env.client)
27+
)
28+
file_id = receipt.file_id
29+
assert file_id is not None, "File ID should not be None"
30+
31+
# Query the file contents
32+
contents = FileContentsQuery().set_file_id(file_id).execute(env.client)
33+
assert contents == FILE_CONTENT, "File contents mismatch"
34+
35+
36+
@pytest.mark.integration
37+
def test_integration_file_contents_query_get_cost(env):
38+
"""Test that the FileContentsQuery can calculate query costs."""
39+
# Create a file
40+
receipt = (
41+
FileCreateTransaction()
42+
.set_keys([env.operator_key.public_key()])
43+
.set_contents(FILE_CONTENT)
44+
.set_transaction_memo("python sdk e2e tests")
45+
.execute(env.client)
46+
)
47+
file_id = receipt.file_id
48+
assert file_id is not None, "File ID should not be None"
49+
50+
# Create the query and get its cost
51+
file_contents = FileContentsQuery().set_file_id(file_id)
52+
53+
cost = file_contents.get_cost(env.client)
54+
55+
# Execute with the exact cost
56+
contents = file_contents.set_query_payment(cost).execute(env.client)
57+
58+
assert contents == FILE_CONTENT, "File contents mismatch"
59+
60+
61+
@pytest.mark.integration
62+
def test_integration_file_contents_query_empty_contents(env):
63+
"""Test that FileContentsQuery can execute with empty contents."""
64+
# Create a file with no contents
65+
receipt = (
66+
FileCreateTransaction()
67+
.set_keys([env.operator_key.public_key()])
68+
.set_transaction_memo("python sdk e2e tests")
69+
.execute(env.client)
70+
)
71+
file_id = receipt.file_id
72+
assert file_id is not None, "File ID should not be None"
73+
74+
# Query the empty file contents
75+
contents = FileContentsQuery().set_file_id(file_id).execute(env.client)
76+
77+
assert contents == b"", "File contents should be empty"
78+
79+
80+
@pytest.mark.integration
81+
def test_integration_file_contents_query_insufficient_payment(env):
82+
"""Test that FileContentsQuery fails with insufficient payment."""
83+
# Create a test file first
84+
receipt = FileCreateTransaction().set_contents(FILE_CONTENT).execute(env.client)
85+
file_id = receipt.file_id
86+
assert file_id is not None, "File ID should not be None"
87+
88+
# Create query and set very low payment
89+
file_contents = FileContentsQuery().set_file_id(file_id)
90+
file_contents.set_query_payment(Hbar.from_tinybars(1)) # Set very low query payment
91+
92+
with pytest.raises(
93+
PrecheckError, match="failed precheck with status: INSUFFICIENT_TX_FEE"
94+
):
95+
file_contents.execute(env.client)
96+
97+
98+
@pytest.mark.integration
99+
def test_integration_file_contents_query_fails_with_invalid_file_id(env):
100+
"""Test that the FileContentsQuery fails with an invalid file ID."""
101+
# Create a file ID that doesn't exist on the network
102+
file_id = FileId(0, 0, 999999999)
103+
104+
with pytest.raises(
105+
PrecheckError, match="failed precheck with status: INVALID_FILE_ID"
106+
):
107+
FileContentsQuery(file_id).execute(env.client)

0 commit comments

Comments
 (0)