Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions src/reportportal/rp_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import requests
from typing import List, Dict, Optional, Any
from datetime import datetime
from loguru import logger


Expand Down Expand Up @@ -45,7 +46,6 @@ def __init__(self, url: str, project: str, token: str):
url: ReportPortal URL (e.g., https://reportportal.example.com)
project: ReportPortal project name
token: ReportPortal API token
logger: Optional logger instance (if not provided, creates a default logger)
"""
self.url = url
self.project = project
Expand All @@ -64,6 +64,60 @@ def build_headers(self) -> Dict[str, str]:
'accept': '*/*'
}

@staticmethod
def normalize_timestamp(timestamp: Optional[Any]) -> Optional[int]:
"""
Normalize timestamp to Unix milliseconds (integer).

ReportPortal API changed from returning Unix milliseconds to ISO format strings.
This method handles both formats for backward compatibility.

Args:
timestamp: Either ISO format string or Unix milliseconds (int/float)

Returns:
Unix timestamp in milliseconds as integer, or None if input is None
"""
if timestamp is None:
return None

# Already in Unix milliseconds format
if isinstance(timestamp, (int, float)):
return int(timestamp)

# ISO format string - convert to Unix milliseconds
if isinstance(timestamp, str):
try:
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
return int(dt.timestamp() * 1000)
except (ValueError, AttributeError) as e:
logger.warning("Failed to parse timestamp '{}': {}", timestamp, e)
return None

logger.warning("Timestamp was unexpected type: {}", type(timestamp))
return None
Comment thread
silvi-t marked this conversation as resolved.

def normalize_timestamps_in_dict(self, data: Dict[str, Any],
timestamp_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""
Normalize timestamp fields in a dictionary.

Args:
data: Dictionary potentially containing timestamp fields
timestamp_fields: List of field names to normalize (default: startTime, endTime)

Returns:
Dictionary with normalized timestamps
"""
if timestamp_fields is None:
timestamp_fields = ['startTime', 'endTime']

for field in timestamp_fields:
if field in data:
data[field] = self.normalize_timestamp(data[field])

return data

def build_url(self, resource: str, **params) -> str:
"""
Build ReportPortal API URL with query parameters.
Expand Down Expand Up @@ -243,7 +297,7 @@ def get_launches(self,
filters: Additional filters as dict (e.g., {'filter.eq.name': 'test'})

Returns:
List of launch dictionaries
List of launch dictionaries with normalized timestamps (Unix milliseconds)

Raises:
ReportPortalAPIError: On API errors
Expand All @@ -261,6 +315,9 @@ def get_launches(self,
data = self._get(endpoint)
launches = data.get('content', [])

# Normalize timestamps in all launches
launches = [self.normalize_timestamps_in_dict(launch) for launch in launches]

logger.debug(f"Retrieved {len(launches)} launches")
return launches

Expand All @@ -272,14 +329,15 @@ def get_launch_by_id(self, launch_id: str) -> Dict:
launch_id: Launch UUID

Returns:
Launch dictionary
Launch dictionary with normalized timestamps (Unix milliseconds)

Raises:
ReportPortalAPIError: On API errors
"""
logger.debug(f"Fetching launch {launch_id}")
endpoint = f'/api/v1/{self.project}/launch/{launch_id}'
return self._get(endpoint)
launch = self._get(endpoint)
return self.normalize_timestamps_in_dict(launch)

# =========================================================================
# Test Item Operations
Expand All @@ -298,7 +356,7 @@ def get_test_items(self,
filters: Additional filters (e.g., {'filter.eq.status': 'FAILED'})

Returns:
List of test item dictionaries
List of test item dictionaries with normalized timestamps (Unix milliseconds)

Raises:
ReportPortalAPIError: On API errors
Expand All @@ -319,6 +377,9 @@ def get_test_items(self,
data = self._get(endpoint)
items = data.get('content', [])

# Normalize timestamps in all test items
items = [self.normalize_timestamps_in_dict(item) for item in items]

logger.debug(f"Retrieved {len(items)} test items")
return items

Expand All @@ -330,14 +391,15 @@ def get_test_item_by_id(self, item_id: str) -> Dict:
item_id: Test item UUID

Returns:
Test item dictionary
Test item dictionary with normalized timestamps (Unix milliseconds)

Raises:
ReportPortalAPIError: On API errors
"""
logger.debug(f"Fetching test item {item_id}")
endpoint = f'/api/v1/{self.project}/item/{item_id}'
return self._get(endpoint)
item = self._get(endpoint)
return self.normalize_timestamps_in_dict(item)

# =========================================================================
# Analysis Operations
Expand Down Expand Up @@ -391,9 +453,8 @@ def create_api_client(url: str, project: str, token: str) -> ReportPortalAPIClie
url: ReportPortal URL
project: ReportPortal project name
token: ReportPortal API token
logger: Optional logger instance

Returns:
Configured ReportPortalAPIClient instance
"""
return ReportPortalAPIClient(url, project, token, logger=logger)
return ReportPortalAPIClient(url, project, token)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
109 changes: 109 additions & 0 deletions tests/unit/test_rp_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
Unit tests for ReportPortal API Client timestamp normalization.
"""

from reportportal.rp_api_client import ReportPortalAPIClient


class TestTimestampNormalization:
"""Test timestamp normalization for API format changes."""

def test_normalize_timestamp_from_integer(self):
"""Test normalizing Unix milliseconds (old format)."""
timestamp_ms = 1640000000000
result = ReportPortalAPIClient.normalize_timestamp(timestamp_ms)
assert result == 1640000000000
assert isinstance(result, int)

def test_normalize_timestamp_from_float(self):
"""Test normalizing Unix milliseconds as float."""
timestamp_ms = 1640000000000.0
result = ReportPortalAPIClient.normalize_timestamp(timestamp_ms)
assert result == 1640000000000
assert isinstance(result, int)

def test_normalize_timestamp_from_iso_string(self):
"""Test normalizing ISO format string (new format)."""
# Without timezone, assumes local time
iso_timestamp = "2021-12-20T13:33:20"
result = ReportPortalAPIClient.normalize_timestamp(iso_timestamp)
# Verify it returns a valid integer timestamp
assert isinstance(result, int)
assert result > 0

def test_normalize_timestamp_from_iso_string_with_z(self):
"""Test normalizing ISO format string with Z suffix."""
# 2021-12-20 13:33:20 UTC = 1640007200000ms
iso_timestamp = "2021-12-20T13:33:20Z"
result = ReportPortalAPIClient.normalize_timestamp(iso_timestamp)
assert result == 1640007200000
assert isinstance(result, int)

def test_normalize_timestamp_from_iso_string_with_timezone(self):
"""Test normalizing ISO format string with timezone."""
# 2021-12-20 13:33:20 UTC = 1640007200000ms
iso_timestamp = "2021-12-20T13:33:20+00:00"
result = ReportPortalAPIClient.normalize_timestamp(iso_timestamp)
assert result == 1640007200000
assert isinstance(result, int)

def test_normalize_timestamp_none(self):
"""Test normalizing None timestamp."""
result = ReportPortalAPIClient.normalize_timestamp(None)
assert result is None

def test_normalize_timestamp_invalid_string(self):
"""Test normalizing invalid timestamp string."""
result = ReportPortalAPIClient.normalize_timestamp("invalid")
assert result is None

def test_normalize_timestamps_in_dict(self):
"""Test normalizing timestamps in a dictionary."""
client = ReportPortalAPIClient("http://example.com", "project", "token")

data = {
'id': 'launch-123',
'name': 'Test Launch',
'startTime': '2021-12-20T13:33:20+00:00',
'endTime': 1640010000000, # Mixed formats
'other_field': 'value'
}

result = client.normalize_timestamps_in_dict(data)

assert result['id'] == 'launch-123'
assert result['name'] == 'Test Launch'
assert result['startTime'] == 1640007200000 # Converted from ISO
assert result['endTime'] == 1640010000000 # Already in milliseconds
assert result['other_field'] == 'value'

def test_normalize_timestamps_custom_fields(self):
"""Test normalizing custom timestamp fields."""
client = ReportPortalAPIClient("http://example.com", "project", "token")

data = {
'createdAt': '2021-12-20T13:33:20+00:00',
'updatedAt': '2021-12-20T14:33:20+00:00',
}

result = client.normalize_timestamps_in_dict(data, ['createdAt', 'updatedAt'])

assert result['createdAt'] == 1640007200000
assert result['updatedAt'] == 1640010800000

def test_normalize_timestamps_missing_fields(self):
"""Test normalizing with missing timestamp fields."""
client = ReportPortalAPIClient("http://example.com", "project", "token")

data = {
'id': 'launch-123',
'name': 'Test Launch',
}

# Should not raise error if fields don't exist
result = client.normalize_timestamps_in_dict(data)

assert result['id'] == 'launch-123'
assert result['name'] == 'Test Launch'
assert 'startTime' not in result
assert 'endTime' not in result
Loading