Skip to content

Commit 6d2ca3c

Browse files
author
Matthias Zimmermann
committed
feat: complete switch to expires_in, add to_blocks, and to_seconds utility functions
1 parent 830a7d9 commit 6d2ca3c

File tree

7 files changed

+187
-41
lines changed

7 files changed

+187
-41
lines changed

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@ entity_key, receipt = client.arkiv.create_entity(
4444
expires_in=1000
4545
)
4646

47-
# Check and print entity key
48-
exists = client.arkiv.entity_exists(entity_key)
49-
print(f"Created entity: {entity_key} (exists={exists}), creation TX: {receipt.tx_hash}")
50-
5147
# Get individual entity and print its details
5248
entity = client.arkiv.get_entity(entity_key)
49+
print(f"Creation TX: {receipt.tx_hash}")
5350
print(f"Entity: {entity}")
5451

5552
# Clean up - delete entity
@@ -148,9 +145,9 @@ provider = ProviderBuilder().kaolin().build()
148145
client = Arkiv(provider, account=bob)
149146

150147
# Additional builder examples
148+
provider_custom = ProviderBuilder().custom("https://mendoza.hoodi.arkiv.network/rpc").build()
151149
provider_container = ProviderBuilder().node().build()
152150
provider_kaolin_ws = ProviderBuilder().kaolin().ws().build()
153-
provider_custom = ProviderBuilder().custom("https://my-rpc.io").build()
154151
```
155152

156153
### Query Iterator

src/arkiv/module_base.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ class ArkivModuleBase(Generic[ClientT]):
7676
EXPIRES_IN_DEFAULT = (
7777
1000 # Default expiration time for created entities (~30 mins with 2s blocks)
7878
)
79+
CONTENT_TYPE_DEFAULT = (
80+
"application/octet-stream" # Default content type for payloads
81+
)
7982

8083
def __init__(
8184
self, client: ClientT, expires_in_default: int = EXPIRES_IN_DEFAULT
@@ -453,15 +456,35 @@ def query_entities_page(
453456
"""
454457
raise NotImplementedError("Subclasses must implement query_entities()")
455458

459+
@staticmethod
460+
def to_seconds(
461+
seconds: int = 0, minutes: int = 0, hours: int = 0, days: int = 0
462+
) -> int:
463+
"""
464+
Convert a time duration to number of seconds.
465+
466+
Useful for calculating expires_in parameters based on desired entity lifetime.
467+
468+
Args:
469+
seconds: Number of seconds
470+
minutes: Number of minutes
471+
hours: Number of hours
472+
days: Number of days
473+
474+
Returns:
475+
Total number of seconds corresponding to the time duration
476+
"""
477+
from arkiv.utils import to_seconds as _to_seconds
478+
479+
return _to_seconds(seconds, minutes, hours, days)
480+
481+
@staticmethod
456482
def to_blocks(
457-
self, seconds: int = 0, minutes: int = 0, hours: int = 0, days: int = 0
483+
seconds: int = 0, minutes: int = 0, hours: int = 0, days: int = 0
458484
) -> int:
459485
"""
460486
Convert a time duration to number of blocks.
461487
462-
Useful for calculating expires_in parameters based on
463-
desired entity lifetime.
464-
465488
Args:
466489
seconds: Number of seconds
467490
minutes: Number of minutes
@@ -471,8 +494,9 @@ def to_blocks(
471494
Returns:
472495
Number of blocks corresponding to the time duration
473496
"""
474-
total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400
475-
return total_seconds // self.BLOCK_TIME_SECONDS
497+
from arkiv.utils import to_blocks as _to_blocks
498+
499+
return _to_blocks(seconds=seconds, minutes=minutes, hours=hours, days=days)
476500

477501
# NOTE: Other public API methods (iterate_entities, watch_entity_*, etc.) could also
478502
# be defined here, but they have more significant differences between sync/async

src/arkiv/utils.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,53 @@
6565
UpdateOp,
6666
)
6767

68-
CONTENT_TYPE_DEFAULT = "application/octet-stream"
69-
EXPIRES_IN_DEFAULT = (
70-
1000 # Default blocks to live for created entities (~30 mins with 2s blocks)
71-
)
72-
7368
logger = logging.getLogger(__name__)
7469

7570

71+
def to_seconds(
72+
seconds: int = 0, minutes: int = 0, hours: int = 0, days: int = 0
73+
) -> int:
74+
"""
75+
Convert a time duration to number of seconds.
76+
77+
Useful for calculating expires_in parameters based on
78+
desired entity lifetime.
79+
80+
Args:
81+
seconds: Number of seconds
82+
minutes: Number of minutes
83+
hours: Number of hours
84+
days: Number of days
85+
86+
Returns:
87+
Number of seconds corresponding to the time duration
88+
"""
89+
total_seconds = seconds + minutes * 60 + hours * 3600 + days * 86400
90+
return total_seconds
91+
92+
93+
def to_blocks(seconds: int = 0, minutes: int = 0, hours: int = 0, days: int = 0) -> int:
94+
"""
95+
Convert a time duration to number of blocks.
96+
97+
Args:
98+
seconds: Number of seconds
99+
minutes: Number of minutes
100+
hours: Number of hours
101+
days: Number of days
102+
103+
Returns:
104+
Number of blocks corresponding to the time duration
105+
"""
106+
# Import here to avoid circular dependency
107+
from arkiv.module_base import ArkivModuleBase
108+
109+
total_seconds = ArkivModuleBase.to_seconds(
110+
seconds=seconds, minutes=minutes, hours=hours, days=days
111+
)
112+
return total_seconds // ArkivModuleBase.BLOCK_TIME_SECONDS
113+
114+
76115
def to_entity_key(entity_key_int: int) -> EntityKey:
77116
hex_value = Web3.to_hex(entity_key_int)
78117
# ensure lenth is 66 (0x + 64 hex)
@@ -129,11 +168,17 @@ def check_and_set_entity_op_defaults(
129168
) -> tuple[bytes, str, Attributes, int]:
130169
"""Check and set defaults for entity management arguments."""
131170
if expires_in is None:
132-
expires_in = EXPIRES_IN_DEFAULT
171+
# Import here to avoid circular dependency
172+
from arkiv.module_base import ArkivModuleBase
173+
174+
expires_in = ArkivModuleBase.EXPIRES_IN_DEFAULT
133175
if not payload:
134176
payload = b""
135177
if not content_type:
136-
content_type = CONTENT_TYPE_DEFAULT
178+
# Import here to avoid circular dependency
179+
from arkiv.module_base import ArkivModuleBase
180+
181+
content_type = ArkivModuleBase.CONTENT_TYPE_DEFAULT
137182
if not attributes:
138183
attributes = Attributes({})
139184

@@ -710,7 +755,7 @@ def rlp_encode_transaction(tx: Operations) -> bytes:
710755
# Create
711756
[
712757
[
713-
element.expires_in,
758+
to_blocks(seconds=element.expires_in),
714759
element.content_type,
715760
element.payload,
716761
*split_attributes(element.attributes),
@@ -722,7 +767,7 @@ def rlp_encode_transaction(tx: Operations) -> bytes:
722767
[
723768
entity_key_to_bytes(element.key),
724769
element.content_type,
725-
element.expires_in,
770+
to_blocks(seconds=element.expires_in),
726771
element.payload,
727772
*split_attributes(element.attributes),
728773
]

tests/test_arkiv_fixture.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from arkiv import Arkiv
88
from arkiv.account import NamedAccount
99
from arkiv.module import ArkivModule
10+
from arkiv.module_base import ArkivModuleBase
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -131,3 +132,42 @@ def test_arkiv_transfer_eth_address(
131132
)
132133

133134
logger.info("Arkiv ETH transfer between accounts succeeded (to: checksum address)")
135+
136+
137+
def test_arkiv_module_base_to_seconds() -> None:
138+
"""Test ArkivModuleBase.to_seconds static method."""
139+
# Test individual units
140+
assert ArkivModuleBase.to_seconds(seconds=120) == 120
141+
assert ArkivModuleBase.to_seconds(minutes=2) == 120
142+
assert ArkivModuleBase.to_seconds(hours=1) == 3600
143+
assert ArkivModuleBase.to_seconds(days=1) == 86400
144+
145+
# Test mixed units
146+
result = ArkivModuleBase.to_seconds(days=1, hours=2, minutes=30, seconds=60)
147+
assert result == 95460 # 86400 + 7200 + 1800 + 60
148+
149+
# Test default (no arguments)
150+
assert ArkivModuleBase.to_seconds() == 0
151+
152+
logger.info("ArkivModuleBase.to_seconds() works correctly")
153+
154+
155+
def test_arkiv_module_base_to_blocks() -> None:
156+
"""Test ArkivModuleBase.to_blocks static method."""
157+
# Test individual units (with 2 second block time)
158+
assert ArkivModuleBase.to_blocks(seconds=120) == 60
159+
assert ArkivModuleBase.to_blocks(minutes=2) == 60
160+
assert ArkivModuleBase.to_blocks(hours=1) == 1800
161+
assert ArkivModuleBase.to_blocks(days=1) == 43200
162+
163+
# Test mixed units
164+
result = ArkivModuleBase.to_blocks(days=1, hours=2, minutes=30, seconds=60)
165+
assert result == 47730 # 95460 seconds / 2 = 47730 blocks
166+
167+
# Test default (no arguments)
168+
assert ArkivModuleBase.to_blocks() == 0
169+
170+
# Test rounding down
171+
assert ArkivModuleBase.to_blocks(seconds=125) == 62 # 125 / 2 = 62.5 -> 62
172+
173+
logger.info("ArkivModuleBase.to_blocks() works correctly")

tests/test_entity_update.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -373,11 +373,13 @@ def test_update_entity_expires_in_extension(self, arkiv_client_http: Arkiv) -> N
373373

374374
# Get initial expiration
375375
entity_before = arkiv_client_http.arkiv.get_entity(entity_key)
376-
initial_expiration = entity_before.expires_at_block
377-
assert initial_expiration is not None, "Entity should have expiration block"
378-
logger.info(f"Initial expiration block: {initial_expiration}")
376+
initial_expiration_block = entity_before.expires_at_block
377+
assert initial_expiration_block is not None, (
378+
"Entity should have expiration block"
379+
)
380+
logger.info(f"Initial expiration block: {initial_expiration_block}")
379381

380-
# Update with higher expires_in
382+
# Update with higher expires_in (in seconds)
381383
new_expires_in = 200
382384
update_tx_hash = arkiv_client_http.arkiv.update_entity(
383385
entity_key,
@@ -387,25 +389,20 @@ def test_update_entity_expires_in_extension(self, arkiv_client_http: Arkiv) -> N
387389
)
388390
check_tx_hash("update_expires_in", update_tx_hash)
389391

390-
# Verify new expiration (expiration extension calculation)
391-
assert entity_before.expires_at_block is not None
392-
expected = replace(
393-
entity_before,
394-
expires_at_block=entity_before.expires_at_block
395-
- initial_expiration
396-
+ new_expires_in,
397-
)
398-
check_entity("update_expires_in", arkiv_client_http, expected)
392+
# Get updated entity
393+
entity_after = arkiv_client_http.arkiv.get_entity(entity_key)
399394

400-
# Verify expiration was extended (should be roughly initial + new_expires_in)
401-
# Note: exact value depends on block advancement during the update
402-
assert expected.expires_at_block is not None
403-
assert expected.expires_at_block > initial_expiration, (
404-
"Expiration should increase with higher expires_in"
395+
# Verify new expiration was extended
396+
# The new expiration should be: current_block + to_blocks(new_expires_in)
397+
# Since we don't know the exact current block, we verify it's greater than initial
398+
assert entity_after.expires_at_block is not None
399+
assert entity_after.expires_at_block > initial_expiration_block, (
400+
f"Expiration should increase with higher expires_in: "
401+
f"{initial_expiration_block} -> {entity_after.expires_at_block}"
405402
)
406403

407404
logger.info(
408-
f"Expiration extension successful: {initial_expiration} -> {expected.expires_at_block}"
405+
f"Expiration extension successful: {initial_expiration_block} -> {entity_after.expires_at_block}"
409406
)
410407

411408
def test_update_entity_both_payload_and_attributes(

tests/test_module_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,47 @@
77
logger = logging.getLogger(__name__)
88

99

10+
class TestToSeconds:
11+
"""Test cases for to_seconds time conversion method."""
12+
13+
def test_to_seconds_from_seconds(self, arkiv_client_http: Arkiv):
14+
"""Test conversion from seconds."""
15+
result = arkiv_client_http.arkiv.to_seconds(seconds=120)
16+
assert result == 120
17+
18+
def test_to_seconds_from_minutes(self, arkiv_client_http: Arkiv):
19+
"""Test conversion from minutes to seconds."""
20+
# 2 minutes = 120 seconds
21+
result = arkiv_client_http.arkiv.to_seconds(minutes=2)
22+
assert result == 120
23+
24+
def test_to_seconds_from_hours(self, arkiv_client_http: Arkiv):
25+
"""Test conversion from hours to seconds."""
26+
# 1 hour = 3600 seconds
27+
result = arkiv_client_http.arkiv.to_seconds(hours=1)
28+
assert result == 3600
29+
30+
def test_to_seconds_from_days(self, arkiv_client_http: Arkiv):
31+
"""Test conversion from days to seconds."""
32+
# 1 day = 86400 seconds
33+
result = arkiv_client_http.arkiv.to_seconds(days=1)
34+
assert result == 86400
35+
36+
def test_to_seconds_mixed_units(self, arkiv_client_http: Arkiv):
37+
"""Test conversion with mixed time units."""
38+
# 1 day + 2 hours + 30 minutes + 60 seconds
39+
# = 86400 + 7200 + 1800 + 60 = 95460 seconds
40+
result = arkiv_client_http.arkiv.to_seconds(
41+
days=1, hours=2, minutes=30, seconds=60
42+
)
43+
assert result == 95460
44+
45+
def test_to_seconds_default_values(self, arkiv_client_http: Arkiv):
46+
"""Test with all default values (should return 0)."""
47+
result = arkiv_client_http.arkiv.to_seconds()
48+
assert result == 0
49+
50+
1051
class TestToBlocks:
1152
"""Test cases for to_blocks time conversion method."""
1253

tests/test_query_select.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
EntityKey,
2222
QueryOptions,
2323
)
24+
from arkiv.utils import to_blocks
2425

25-
EXPIRES_IN = 100
26+
EXPIRES_IN = 100 # In seconds
27+
EXPIRES_IN_BLOCKS = to_blocks(seconds=EXPIRES_IN) # Convert to blocks
2628
CONTENT_TYPE_VALUE = "text/plain"
2729

2830

@@ -193,7 +195,7 @@ def test_query_select_all_fields(self, arkiv_client_http: Arkiv) -> None:
193195
expected_created_at + 1 if expected_created_at else None
194196
)
195197
expected_expires_at = (
196-
expected_last_modified_at + EXPIRES_IN
198+
expected_last_modified_at + EXPIRES_IN_BLOCKS
197199
if expected_last_modified_at
198200
else None
199201
)

0 commit comments

Comments
 (0)