fix: Clean up whitespace and improve code formatting across service adapters
Some checks failed
Integration Tests / integration-tests (push) Failing after 20s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 24s
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 25s
Service Adapters (Python FastAPI) / test (3.13) (push) Failing after 25s
Service Adapters (Python FastAPI) / build (push) Has been skipped
Some checks failed
Integration Tests / integration-tests (push) Failing after 20s
Integration Tests / performance-tests (push) Has been skipped
Service Adapters (Python FastAPI) / test (3.11) (push) Failing after 24s
Service Adapters (Python FastAPI) / test (3.12) (push) Failing after 25s
Service Adapters (Python FastAPI) / test (3.13) (push) Failing after 25s
Service Adapters (Python FastAPI) / build (push) Has been skipped
### Summary of Changes - Removed unnecessary whitespace and standardized formatting in multiple files, including `main.py`, `logging_middleware.py`, `general.py`, and various health checker implementations. - Enhanced readability and maintainability of the codebase by ensuring consistent formatting practices. ### Expected Results - Improved code clarity, making it easier for developers to read and understand the service adapters' code. - Streamlined the codebase, facilitating future updates and maintenance.
This commit is contained in:
@@ -12,6 +12,7 @@ from services.status_checker import status_checker
|
||||
# Set up unified logging for both application and request logs
|
||||
setup_logging(level="INFO", enable_request_logging=True)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Manage application lifespan events."""
|
||||
|
||||
@@ -5,7 +5,6 @@ This module provides custom logging middleware for FastAPI requests
|
||||
to ensure consistent logging format with application logs.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
@@ -41,10 +40,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
user_agent = request.headers.get("user-agent", "unknown")
|
||||
|
||||
# Log request start
|
||||
logger.info(
|
||||
f"Request started: {method} {url_path} from {client_ip} "
|
||||
f"(User-Agent: {user_agent})"
|
||||
)
|
||||
logger.info(f"Request started: {method} {url_path} from {client_ip} " f"(User-Agent: {user_agent})")
|
||||
|
||||
try:
|
||||
# Process the request
|
||||
@@ -54,10 +50,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# Log successful response
|
||||
logger.info(
|
||||
f"Request completed: {method} {url_path} -> "
|
||||
f"{response.status_code} in {process_time:.3f}s"
|
||||
)
|
||||
logger.info(f"Request completed: {method} {url_path} -> " f"{response.status_code} in {process_time:.3f}s")
|
||||
|
||||
return response
|
||||
|
||||
@@ -66,10 +59,7 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# Log error
|
||||
logger.error(
|
||||
f"Request failed: {method} {url_path} -> "
|
||||
f"Exception: {str(e)} in {process_time:.3f}s"
|
||||
)
|
||||
logger.error(f"Request failed: {method} {url_path} -> " f"Exception: {str(e)} in {process_time:.3f}s")
|
||||
|
||||
# Re-raise the exception
|
||||
raise
|
||||
|
||||
@@ -54,11 +54,13 @@ async def debug_logging():
|
||||
|
||||
# Test request logger
|
||||
from services.logging_config import get_request_logger
|
||||
|
||||
request_logger = get_request_logger()
|
||||
request_logger.info("This is a request logger message")
|
||||
|
||||
# Test application logger
|
||||
from services.logging_config import get_application_logger
|
||||
|
||||
app_logger = get_application_logger()
|
||||
app_logger.info("This is an application logger message")
|
||||
|
||||
@@ -76,11 +78,7 @@ async def debug_logging():
|
||||
}
|
||||
|
||||
logger.info("Unified logging debug info requested")
|
||||
return {
|
||||
"message": "Unified log messages sent to console",
|
||||
"config": config_info,
|
||||
"note": "All logs now use the same format and handler"
|
||||
}
|
||||
return {"message": "Unified log messages sent to console", "config": config_info, "note": "All logs now use the same format and handler"}
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -108,12 +106,7 @@ async def debug_sensor(service_name: str):
|
||||
# Get raw sensor data
|
||||
result = await checker.check_health(service_name, config)
|
||||
|
||||
return {
|
||||
"service_name": service_name,
|
||||
"config": config,
|
||||
"result": result.to_dict(),
|
||||
"raw_sensor_data": result.metadata
|
||||
}
|
||||
return {"service_name": service_name, "config": config, "result": result.to_dict(), "raw_sensor_data": result.metadata}
|
||||
except Exception as e:
|
||||
logger.error(f"Error debugging sensor for {service_name}: {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -143,7 +136,7 @@ async def get_services():
|
||||
response_time=status_info.get("response_time"),
|
||||
error=status_info.get("error"),
|
||||
uptime=status_info.get("uptime"),
|
||||
metadata=status_info.get("metadata", {})
|
||||
metadata=status_info.get("metadata", {}),
|
||||
)
|
||||
|
||||
logger.info(f"Service status check completed - returning status for {len(service_status)} services")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from operator import truediv
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -10,7 +9,12 @@ load_dotenv()
|
||||
SERVICES = {
|
||||
"home_assistant": {
|
||||
"url": os.getenv("HOME_ASSISTANT_URL", "http://192.168.2.158:8123"),
|
||||
"token": os.getenv("HOME_ASSISTANT_TOKEN", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3MjdiY2QwMjNkNmM0NzgzYmRiMzg2ZDYxYzQ3N2NmYyIsImlhdCI6MTc1ODE4MDg2MiwiZXhwIjoyMDczNTQwODYyfQ.rN_dBtYmXIo4J1DffgWb6G0KLsgaQ6_kH-kiWJeQQQM"),
|
||||
"token": os.getenv(
|
||||
"HOME_ASSISTANT_TOKEN",
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
||||
"eyJpc3MiOiI3MjdiY2QwMjNkNmM0NzgzYmRiMzg2ZDYxYzQ3N2NmYyIsImlhdCI6MTc1ODE4MDg2MiwiZXhwIjoyMDczNTQwODYyfQ."
|
||||
"rN_dBtYmXIo4J1DffgWb6G0KLsgaQ6_kH-kiWJeQQQM",
|
||||
),
|
||||
"enabled": True,
|
||||
"health_check_type": "sensor", # Use sensor-based health checking
|
||||
"sensor_entity": "sensor.uptime_34", # Check uptime sensor
|
||||
|
||||
@@ -11,20 +11,17 @@ from typing import Any, Dict, Optional
|
||||
import httpx
|
||||
from httpx import HTTPError, TimeoutException
|
||||
|
||||
from .base import BaseHealthChecker, HealthCheckResult
|
||||
from utils.time_formatter import format_uptime_for_frontend
|
||||
|
||||
from .base import BaseHealthChecker, HealthCheckResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIHealthChecker(BaseHealthChecker):
|
||||
"""Health checker for services with API health endpoints."""
|
||||
|
||||
async def check_health(
|
||||
self,
|
||||
service_name: str,
|
||||
config: Dict[str, Any]
|
||||
) -> HealthCheckResult:
|
||||
async def check_health(self, service_name: str, config: Dict[str, Any]) -> HealthCheckResult:
|
||||
"""
|
||||
Check health via API endpoint.
|
||||
|
||||
@@ -70,11 +67,7 @@ class APIHealthChecker(BaseHealthChecker):
|
||||
uptime_info = self._extract_uptime_from_response(response, service_name)
|
||||
formatted_uptime = format_uptime_for_frontend(uptime_info)
|
||||
|
||||
metadata = {
|
||||
"http_status": response.status_code,
|
||||
"response_size": len(response.content),
|
||||
"health_status": health_status
|
||||
}
|
||||
metadata = {"http_status": response.status_code, "response_size": len(response.content), "health_status": health_status}
|
||||
return HealthCheckResult("healthy", response_time, metadata=metadata, uptime=formatted_uptime)
|
||||
elif response.status_code == 401:
|
||||
logger.warning(f"Service {service_name} returned 401 - authentication required")
|
||||
|
||||
@@ -7,7 +7,7 @@ health checking strategies.
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -23,7 +23,7 @@ class HealthCheckResult:
|
||||
response_time: Optional[float] = None,
|
||||
error: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
uptime: Optional[str] = None
|
||||
uptime: Optional[str] = None,
|
||||
):
|
||||
self.status = status
|
||||
self.response_time = response_time
|
||||
@@ -33,13 +33,7 @@ class HealthCheckResult:
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert result to dictionary."""
|
||||
return {
|
||||
"status": self.status,
|
||||
"response_time": self.response_time,
|
||||
"error": self.error,
|
||||
"uptime": self.uptime,
|
||||
"metadata": self.metadata
|
||||
}
|
||||
return {"status": self.status, "response_time": self.response_time, "error": self.error, "uptime": self.uptime, "metadata": self.metadata}
|
||||
|
||||
|
||||
class BaseHealthChecker(ABC):
|
||||
@@ -57,11 +51,7 @@ class BaseHealthChecker(ABC):
|
||||
logger.debug(f"Initialized {self.__class__.__name__} with timeout: {timeout}s")
|
||||
|
||||
@abstractmethod
|
||||
async def check_health(
|
||||
self,
|
||||
service_name: str,
|
||||
config: Dict[str, Any]
|
||||
) -> HealthCheckResult:
|
||||
async def check_health(self, service_name: str, config: Dict[str, Any]) -> HealthCheckResult:
|
||||
"""
|
||||
Check the health of a service.
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@ import logging
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import httpx
|
||||
from httpx import HTTPError, TimeoutException
|
||||
|
||||
from .base import BaseHealthChecker, HealthCheckResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -20,11 +17,7 @@ logger = logging.getLogger(__name__)
|
||||
class CustomHealthChecker(BaseHealthChecker):
|
||||
"""Health checker for services requiring custom health check logic."""
|
||||
|
||||
async def check_health(
|
||||
self,
|
||||
service_name: str,
|
||||
config: Dict[str, Any]
|
||||
) -> HealthCheckResult:
|
||||
async def check_health(self, service_name: str, config: Dict[str, Any]) -> HealthCheckResult:
|
||||
"""
|
||||
Check health using custom logic.
|
||||
|
||||
@@ -63,16 +56,12 @@ class CustomHealthChecker(BaseHealthChecker):
|
||||
metadata = {
|
||||
"total_checks": len(health_checks),
|
||||
"check_results": [result.to_dict() for result in results],
|
||||
"overall_response_time": overall_response_time
|
||||
"overall_response_time": overall_response_time,
|
||||
}
|
||||
|
||||
return HealthCheckResult(overall_status, overall_response_time, metadata=metadata)
|
||||
|
||||
async def _run_single_check(
|
||||
self,
|
||||
service_name: str,
|
||||
check_config: Dict[str, Any]
|
||||
) -> HealthCheckResult:
|
||||
async def _run_single_check(self, service_name: str, check_config: Dict[str, Any]) -> HealthCheckResult:
|
||||
"""
|
||||
Run a single health check.
|
||||
|
||||
@@ -155,7 +144,7 @@ class CustomHealthChecker(BaseHealthChecker):
|
||||
return "error"
|
||||
|
||||
# Count statuses
|
||||
status_counts = {}
|
||||
status_counts: Dict[str, int] = {}
|
||||
for result in results:
|
||||
status = result.status
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
@@ -5,7 +5,7 @@ This module provides a registry and factory for different health checker types.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Type
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from .api_checker import APIHealthChecker
|
||||
from .base import BaseHealthChecker
|
||||
@@ -70,7 +70,7 @@ class HealthCheckerRegistry:
|
||||
class HealthCheckerFactory:
|
||||
"""Factory for creating health checker instances."""
|
||||
|
||||
def __init__(self, registry: HealthCheckerRegistry = None):
|
||||
def __init__(self, registry: Optional[HealthCheckerRegistry] = None):
|
||||
"""
|
||||
Initialize the factory.
|
||||
|
||||
@@ -80,11 +80,7 @@ class HealthCheckerFactory:
|
||||
self.registry = registry or HealthCheckerRegistry()
|
||||
logger.debug("Initialized health checker factory")
|
||||
|
||||
def create_checker(
|
||||
self,
|
||||
checker_type: str,
|
||||
timeout: float = 5.0
|
||||
) -> BaseHealthChecker:
|
||||
def create_checker(self, checker_type: str, timeout: float = 5.0) -> BaseHealthChecker:
|
||||
"""
|
||||
Create a health checker instance.
|
||||
|
||||
@@ -100,12 +96,7 @@ class HealthCheckerFactory:
|
||||
logger.debug(f"Created {checker_type} health checker with timeout {timeout}s")
|
||||
return checker
|
||||
|
||||
def create_checker_for_service(
|
||||
self,
|
||||
service_name: str,
|
||||
config: Dict[str, Any],
|
||||
timeout: float = 5.0
|
||||
) -> BaseHealthChecker:
|
||||
def create_checker_for_service(self, service_name: str, config: Dict[str, Any], timeout: float = 5.0) -> BaseHealthChecker:
|
||||
"""
|
||||
Create a health checker for a specific service based on its configuration.
|
||||
|
||||
|
||||
@@ -9,23 +9,19 @@ import logging
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
from httpx import HTTPError, TimeoutException
|
||||
|
||||
from .base import BaseHealthChecker, HealthCheckResult
|
||||
from utils.time_formatter import format_uptime_for_frontend
|
||||
|
||||
from .base import BaseHealthChecker, HealthCheckResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SensorHealthChecker(BaseHealthChecker):
|
||||
"""Health checker for services with sensor-based health information."""
|
||||
|
||||
async def check_health(
|
||||
self,
|
||||
service_name: str,
|
||||
config: Dict[str, Any]
|
||||
) -> HealthCheckResult:
|
||||
async def check_health(self, service_name: str, config: Dict[str, Any]) -> HealthCheckResult:
|
||||
"""
|
||||
Check health via sensor data.
|
||||
|
||||
@@ -86,7 +82,7 @@ class SensorHealthChecker(BaseHealthChecker):
|
||||
"sensor_state": sensor_data.get("state"),
|
||||
"sensor_attributes": sensor_data.get("attributes", {}),
|
||||
"last_updated": sensor_data.get("last_updated"),
|
||||
"entity_id": sensor_data.get("entity_id")
|
||||
"entity_id": sensor_data.get("entity_id"),
|
||||
}
|
||||
|
||||
return HealthCheckResult(health_status, response_time, metadata=metadata, uptime=formatted_uptime)
|
||||
@@ -138,10 +134,12 @@ class SensorHealthChecker(BaseHealthChecker):
|
||||
# Timestamp sensor - if it has a valid timestamp, service is healthy
|
||||
try:
|
||||
from datetime import datetime
|
||||
|
||||
# Try to parse the timestamp
|
||||
parsed_time = datetime.fromisoformat(state.replace('Z', '+00:00'))
|
||||
parsed_time = datetime.fromisoformat(state.replace("Z", "+00:00"))
|
||||
# If we can parse it and it's recent (within last 24 hours), it's healthy
|
||||
from datetime import datetime, timezone
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
time_diff = now - parsed_time
|
||||
is_healthy = time_diff.total_seconds() < 86400 # 24 hours
|
||||
|
||||
@@ -14,10 +14,7 @@ DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno
|
||||
|
||||
|
||||
def setup_logging(
|
||||
level: str = "INFO",
|
||||
format_string: Optional[str] = None,
|
||||
include_timestamp: bool = True,
|
||||
enable_request_logging: bool = True
|
||||
level: str = "INFO", format_string: Optional[str] = None, include_timestamp: bool = True, enable_request_logging: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Set up unified logging configuration for the application and requests.
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Dict
|
||||
|
||||
from services.config import SERVICES
|
||||
from services.health_checkers import factory
|
||||
from services.health_checkers.base import BaseHealthChecker
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -27,7 +28,7 @@ class ServiceStatusChecker:
|
||||
timeout: Request timeout in seconds
|
||||
"""
|
||||
self.timeout = timeout
|
||||
self.checkers = {} # Cache for checker instances
|
||||
self.checkers: Dict[str, BaseHealthChecker] = {} # Cache for checker instances
|
||||
logger.info(f"ServiceStatusChecker initialized with timeout: {timeout}s")
|
||||
|
||||
async def check_service_health(self, service_name: str, config: Dict) -> Dict:
|
||||
@@ -45,12 +46,7 @@ class ServiceStatusChecker:
|
||||
|
||||
if not config.get("enabled", False):
|
||||
logger.debug(f"Service {service_name} is disabled, skipping health check")
|
||||
return {
|
||||
"status": "disabled",
|
||||
"response_time": None,
|
||||
"error": None,
|
||||
"metadata": {}
|
||||
}
|
||||
return {"status": "disabled", "response_time": None, "error": None, "metadata": {}}
|
||||
|
||||
try:
|
||||
# Get or create checker for this service
|
||||
@@ -64,12 +60,7 @@ class ServiceStatusChecker:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error checking {service_name}: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"response_time": None,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"metadata": {}
|
||||
}
|
||||
return {"status": "error", "response_time": None, "error": f"Unexpected error: {str(e)}", "metadata": {}}
|
||||
|
||||
async def _get_checker_for_service(self, service_name: str, config: Dict):
|
||||
"""
|
||||
@@ -109,28 +100,32 @@ class ServiceStatusChecker:
|
||||
logger.debug(f"Created {len(tasks)} concurrent health check tasks")
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
service_status = {}
|
||||
service_status: Dict[str, Dict] = {}
|
||||
healthy_count = 0
|
||||
error_count = 0
|
||||
|
||||
for service_name, result in zip(service_names, results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Exception during health check for {service_name}: {str(result)}")
|
||||
service_status[service_name] = {
|
||||
"status": "error",
|
||||
"response_time": None,
|
||||
"error": f"Exception: {str(result)}",
|
||||
"metadata": {}
|
||||
}
|
||||
service_status[service_name] = {"status": "error", "response_time": None, "error": f"Exception: {str(result)}", "metadata": {}}
|
||||
error_count += 1
|
||||
else:
|
||||
# result is a Dict at this point, but we need to ensure it's actually a dict
|
||||
if isinstance(result, dict):
|
||||
service_status[service_name] = result
|
||||
if result["status"] == "healthy":
|
||||
if result.get("status") == "healthy":
|
||||
healthy_count += 1
|
||||
elif result["status"] in ["error", "timeout", "unhealthy"]:
|
||||
elif result.get("status") in ["error", "timeout", "unhealthy"]:
|
||||
error_count += 1
|
||||
else:
|
||||
# This shouldn't happen, but handle it gracefully
|
||||
logger.error(f"Unexpected result type for {service_name}: {type(result)}")
|
||||
service_status[service_name] = {"status": "error", "response_time": None, "error": "Unexpected result type", "metadata": {}}
|
||||
error_count += 1
|
||||
|
||||
logger.info(f"Health check completed: {healthy_count} healthy, {error_count} errors, {len(SERVICES) - healthy_count - error_count} other statuses")
|
||||
logger.info(
|
||||
f"Health check completed: {healthy_count} healthy, {error_count} errors, " f"{len(SERVICES) - healthy_count - error_count} other statuses"
|
||||
)
|
||||
return service_status
|
||||
|
||||
async def close(self):
|
||||
|
||||
@@ -4,7 +4,7 @@ Utilities Package
|
||||
This package contains utility functions for the service adapters.
|
||||
"""
|
||||
|
||||
from .time_formatter import format_uptime_for_frontend, format_response_time
|
||||
from .time_formatter import format_response_time, format_uptime_for_frontend
|
||||
|
||||
__all__ = [
|
||||
"format_uptime_for_frontend",
|
||||
|
||||
@@ -50,7 +50,7 @@ def format_uptime_for_frontend(uptime_value: Optional[str]) -> str:
|
||||
def _is_timestamp(value: str) -> bool:
|
||||
"""Check if value is an ISO timestamp."""
|
||||
try:
|
||||
datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
@@ -68,7 +68,7 @@ def _is_epoch(value: str) -> bool:
|
||||
def _is_duration_string(value: str) -> bool:
|
||||
"""Check if value is a duration string like '2h 30m' or '5d 2h 15m'."""
|
||||
# Look for patterns like "2h 30m", "5d 2h 15m", "1d 2h 3m 4s"
|
||||
pattern = r'^\d+[dhms]\s*(\d+[dhms]\s*)*$'
|
||||
pattern = r"^\d+[dhms]\s*(\d+[dhms]\s*)*$"
|
||||
return bool(re.match(pattern, value.strip()))
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ def _format_timestamp_uptime(timestamp: str) -> str:
|
||||
"""Format timestamp uptime (time since timestamp)."""
|
||||
try:
|
||||
# Parse timestamp
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
|
||||
@@ -131,22 +131,22 @@ def _parse_duration_string(duration: str) -> float:
|
||||
total_seconds = 0
|
||||
|
||||
# Extract days
|
||||
days_match = re.search(r'(\d+)d', duration)
|
||||
days_match = re.search(r"(\d+)d", duration)
|
||||
if days_match:
|
||||
total_seconds += int(days_match.group(1)) * 86400
|
||||
|
||||
# Extract hours
|
||||
hours_match = re.search(r'(\d+)h', duration)
|
||||
hours_match = re.search(r"(\d+)h", duration)
|
||||
if hours_match:
|
||||
total_seconds += int(hours_match.group(1)) * 3600
|
||||
|
||||
# Extract minutes
|
||||
minutes_match = re.search(r'(\d+)m', duration)
|
||||
minutes_match = re.search(r"(\d+)m", duration)
|
||||
if minutes_match:
|
||||
total_seconds += int(minutes_match.group(1)) * 60
|
||||
|
||||
# Extract seconds
|
||||
seconds_match = re.search(r'(\d+)s', duration)
|
||||
seconds_match = re.search(r"(\d+)s", duration)
|
||||
if seconds_match:
|
||||
total_seconds += int(seconds_match.group(1))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user