Skip to content

Commit 46a8149

Browse files
committed
test and fix
1 parent 882c75d commit 46a8149

File tree

4 files changed

+125
-57
lines changed

4 files changed

+125
-57
lines changed

dune_client/api/usage.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,31 @@ class UsageAPI(BaseRouter):
1717

1818
def get_usage(
1919
self,
20-
start_date: str,
21-
end_date: str,
20+
start_date: str | None = None,
21+
end_date: str | None = None,
2222
) -> UsageResponse:
2323
"""
24-
Get credits usage data, overage, number of private queries run, storage, etc.
25-
over a specific timeframe.
24+
Get credits usage data, billing periods, storage, etc.
2625
https://docs.dune.com/api-reference/usage/endpoint/get-usage
2726
2827
Args:
29-
start_date: Start date for the usage period (ISO format: YYYY-MM-DD)
30-
end_date: End date for the usage period (ISO format: YYYY-MM-DD)
28+
start_date: Optional start date for the usage period (format: YYYY-MM-DD)
29+
end_date: Optional end date for the usage period (format: YYYY-MM-DD)
3130
3231
Returns:
33-
UsageResponse containing usage statistics
32+
UsageResponse containing usage statistics and billing periods
3433
3534
Requires Plus subscription!
3635
"""
37-
params = {
38-
"start_date": start_date,
39-
"end_date": end_date,
40-
}
41-
response_json = self._get(
36+
payload = {}
37+
if start_date:
38+
payload["start_date"] = start_date
39+
if end_date:
40+
payload["end_date"] = end_date
41+
42+
response_json = self._post(
4243
route="/usage",
43-
params=params,
44+
params=payload,
4445
)
4546
try:
4647
return UsageResponse.from_dict(response_json)

dune_client/models.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -399,48 +399,84 @@ class ClearTableResult(DataClassJsonMixin):
399399
message: str
400400

401401

402+
@dataclass
403+
class BillingPeriod:
404+
"""Billing period information with credits and dates"""
405+
406+
credits_included: float
407+
credits_used: float
408+
start_date: str
409+
end_date: str
410+
411+
@classmethod
412+
def from_dict(cls, data: dict[str, Any]) -> BillingPeriod:
413+
"""Constructor from dictionary."""
414+
return cls(
415+
credits_included=float(data["credits_included"]),
416+
credits_used=float(data["credits_used"]),
417+
start_date=data["start_date"],
418+
end_date=data["end_date"],
419+
)
420+
421+
402422
@dataclass
403423
class UsageResponse:
404424
"""
405-
Representation of Response from Dune's [GET] Usage endpoint
425+
Representation of Response from Dune's [POST] Usage endpoint
406426
https://docs.dune.com/api-reference/usage/endpoint/get-usage
407427
"""
408428

409-
credits_used: int
410-
overage_credits: int
411-
private_query_executions: int
412-
storage_bytes: int
429+
billing_periods: list[BillingPeriod]
430+
bytes_allowed: int
431+
bytes_used: int
432+
private_dashboards: int
433+
private_queries: int
413434

414435
@classmethod
415436
def from_dict(cls, data: dict[str, Any]) -> UsageResponse:
416437
"""Constructor from dictionary."""
438+
billing_periods_data = data.get("billing_periods", [])
417439
return cls(
418-
credits_used=int(data.get("credits_used", 0)),
419-
overage_credits=int(data.get("overage_credits", 0)),
420-
private_query_executions=int(data.get("private_query_executions", 0)),
421-
storage_bytes=int(data.get("storage_bytes", 0)),
440+
billing_periods=[BillingPeriod.from_dict(bp) for bp in billing_periods_data],
441+
bytes_allowed=int(data.get("bytes_allowed", 0)),
442+
bytes_used=int(data.get("bytes_used", 0)),
443+
private_dashboards=int(data.get("private_dashboards", 0)),
444+
private_queries=int(data.get("private_queries", 0)),
422445
)
423446

424447

425448
@dataclass
426449
class TableInfo:
427450
"""Information about a single table"""
428451

429-
namespace: str
430-
table_name: str
431452
full_name: str
432-
created_at: str
433453
is_private: bool
454+
created_at: str
455+
# Optional fields that may be present
456+
table_size_bytes: str | None = None
457+
updated_at: str | None = None
458+
459+
@property
460+
def namespace(self) -> str:
461+
"""Extract namespace from full_name (e.g., 'dune.namespace.table' -> 'namespace')"""
462+
parts = self.full_name.split(".")
463+
return parts[1] if len(parts) >= 3 else ""
464+
465+
@property
466+
def table_name(self) -> str:
467+
"""Extract table name from full_name (e.g., 'dune.namespace.table' -> 'table')"""
468+
parts = self.full_name.split(".")
469+
return parts[2] if len(parts) >= 3 else ""
434470

435471
@classmethod
436472
def from_dict(cls, data: dict[str, Any]) -> TableInfo:
437473
"""Constructor from dictionary."""
438474
return cls(
439-
namespace=data["namespace"],
440-
table_name=data["table_name"],
441475
full_name=data["full_name"],
442-
created_at=data["created_at"],
443476
is_private=data["is_private"],
477+
created_at=data["created_at"],
478+
table_size_bytes=data.get("table_size_bytes"),
479+
updated_at=data.get("updated_at"),
444480
)
445481

446482

tests/e2e/test_client.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -398,17 +398,26 @@ def test_run_sql(self):
398398
def test_get_usage(self):
399399
"""Test the get_usage endpoint"""
400400
dune = DuneClient()
401-
usage = dune.get_usage("2024-01-01", "2024-01-31")
401+
usage = dune.get_usage("2025-01-01", "2025-02-01")
402402
# Verify response structure
403-
assert hasattr(usage, "credits_used")
404-
assert hasattr(usage, "overage_credits")
405-
assert hasattr(usage, "private_query_executions")
406-
assert hasattr(usage, "storage_bytes")
407-
# All should be integers
408-
assert isinstance(usage.credits_used, int)
409-
assert isinstance(usage.overage_credits, int)
410-
assert isinstance(usage.private_query_executions, int)
411-
assert isinstance(usage.storage_bytes, int)
403+
assert hasattr(usage, "billing_periods")
404+
assert hasattr(usage, "bytes_allowed")
405+
assert hasattr(usage, "bytes_used")
406+
assert hasattr(usage, "private_dashboards")
407+
assert hasattr(usage, "private_queries")
408+
# Check types
409+
assert isinstance(usage.billing_periods, list)
410+
assert isinstance(usage.bytes_allowed, int)
411+
assert isinstance(usage.bytes_used, int)
412+
assert isinstance(usage.private_dashboards, int)
413+
assert isinstance(usage.private_queries, int)
414+
# If there are billing periods, verify their structure
415+
if usage.billing_periods:
416+
bp = usage.billing_periods[0]
417+
assert hasattr(bp, "credits_included")
418+
assert hasattr(bp, "credits_used")
419+
assert hasattr(bp, "start_date")
420+
assert hasattr(bp, "end_date")
412421

413422
@unittest.skip("Requires Plus subscription and uploaded tables")
414423
def test_list_tables(self):

tests/unit/test_models.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -285,56 +285,74 @@ def test_create_table_result_missing_already_existed_field(self):
285285
def test_usage_response_parsing(self):
286286
"""Test UsageResponse parsing from API response"""
287287
response_data = {
288-
"credits_used": 1000,
289-
"overage_credits": 50,
290-
"private_query_executions": 25,
291-
"storage_bytes": 1024000,
288+
"billing_periods": [
289+
{
290+
"credits_included": 100.0,
291+
"credits_used": 50.5,
292+
"start_date": "2025-01-01",
293+
"end_date": "2025-02-01",
294+
},
295+
{
296+
"credits_included": 100.0,
297+
"credits_used": 75.25,
298+
"start_date": "2025-02-01",
299+
"end_date": "2025-03-01",
300+
},
301+
],
302+
"bytes_allowed": 1024000,
303+
"bytes_used": 512000,
304+
"private_dashboards": 5,
305+
"private_queries": 10,
292306
}
293307
result = UsageResponse.from_dict(response_data)
294-
assert result.credits_used == 1000
295-
assert result.overage_credits == 50
296-
assert result.private_query_executions == 25
297-
assert result.storage_bytes == 1024000
308+
assert len(result.billing_periods) == 2
309+
assert result.billing_periods[0].credits_included == 100.0
310+
assert result.billing_periods[0].credits_used == 50.5
311+
assert result.billing_periods[0].start_date == "2025-01-01"
312+
assert result.billing_periods[1].credits_used == 75.25
313+
assert result.bytes_allowed == 1024000
314+
assert result.bytes_used == 512000
315+
assert result.private_dashboards == 5
316+
assert result.private_queries == 10
298317

299318
def test_usage_response_parsing_with_missing_fields(self):
300319
"""Test UsageResponse parsing with missing optional fields defaults to 0"""
301320
response_data = {}
302321
result = UsageResponse.from_dict(response_data)
303-
assert result.credits_used == 0
304-
assert result.overage_credits == 0
305-
assert result.private_query_executions == 0
306-
assert result.storage_bytes == 0
322+
assert len(result.billing_periods) == 0
323+
assert result.bytes_allowed == 0
324+
assert result.bytes_used == 0
325+
assert result.private_dashboards == 0
326+
assert result.private_queries == 0
307327

308328
def test_table_info_parsing(self):
309329
"""Test TableInfo parsing from API response"""
310330
response_data = {
311-
"namespace": "my_namespace",
312-
"table_name": "my_table",
313331
"full_name": "dune.my_namespace.my_table",
314332
"created_at": "2024-01-15T10:30:00Z",
315333
"is_private": True,
334+
"table_size_bytes": "1024",
335+
"updated_at": "2024-01-16T10:30:00Z",
316336
}
317337
result = TableInfo.from_dict(response_data)
338+
assert result.full_name == "dune.my_namespace.my_table"
318339
assert result.namespace == "my_namespace"
319340
assert result.table_name == "my_table"
320-
assert result.full_name == "dune.my_namespace.my_table"
321341
assert result.created_at == "2024-01-15T10:30:00Z"
322342
assert result.is_private is True
343+
assert result.table_size_bytes == "1024"
344+
assert result.updated_at == "2024-01-16T10:30:00Z"
323345

324346
def test_list_tables_response_parsing(self):
325347
"""Test ListTablesResponse parsing from API response"""
326348
response_data = {
327349
"tables": [
328350
{
329-
"namespace": "namespace1",
330-
"table_name": "table1",
331351
"full_name": "dune.namespace1.table1",
332352
"created_at": "2024-01-15T10:30:00Z",
333353
"is_private": False,
334354
},
335355
{
336-
"namespace": "namespace2",
337-
"table_name": "table2",
338356
"full_name": "dune.namespace2.table2",
339357
"created_at": "2024-01-16T11:30:00Z",
340358
"is_private": True,
@@ -344,7 +362,11 @@ def test_list_tables_response_parsing(self):
344362
}
345363
result = ListTablesResponse.from_dict(response_data)
346364
assert len(result.tables) == 2
365+
assert result.tables[0].full_name == "dune.namespace1.table1"
366+
assert result.tables[0].namespace == "namespace1"
347367
assert result.tables[0].table_name == "table1"
368+
assert result.tables[1].full_name == "dune.namespace2.table2"
369+
assert result.tables[1].namespace == "namespace2"
348370
assert result.tables[1].table_name == "table2"
349371
assert result.next_offset == 100
350372

0 commit comments

Comments
 (0)