bug fixes and performance improvements
All checks were successful
Build Docker Image / build (push) Successful in 45s

This commit is contained in:
Tudor Sitaru
2025-11-11 11:28:01 +00:00
parent acfb22cbea
commit 4f73b3036e
5 changed files with 82 additions and 84 deletions

View File

@@ -75,7 +75,6 @@ curl 'https://api.parentzone.me/v1/posts?typeIDs[]=15&dateFrom=2021-10-18&dateTo
-**Endpoint**: `/v1/posts` -**Endpoint**: `/v1/posts`
-**Type ID filtering**: `typeIDs[]=15` (configurable) -**Type ID filtering**: `typeIDs[]=15` (configurable)
-**Date range filtering**: `dateFrom` and `dateTo` parameters -**Date range filtering**: `dateFrom` and `dateTo` parameters
-**Pagination**: `page` and `per_page` parameters
-**All required headers** from curl command -**All required headers** from curl command
-**Authentication**: `x-api-key` header support -**Authentication**: `x-api-key` header support

View File

@@ -8,7 +8,6 @@ and manages session tokens for API requests.
import asyncio import asyncio
import aiohttp import aiohttp
import json
import logging import logging
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -22,7 +21,7 @@ class AuthManager:
Args: Args:
api_url: Base URL of the API api_url: Base URL of the API
""" """
self.api_url = api_url.rstrip('/') self.api_url = api_url.rstrip("/")
self.login_url = urljoin(self.api_url, "/v1/auth/login") self.login_url = urljoin(self.api_url, "/v1/auth/login")
self.create_session_url = urljoin(self.api_url, "/v1/auth/create-session") self.create_session_url = urljoin(self.api_url, "/v1/auth/create-session")
self.session_token: Optional[str] = None self.session_token: Optional[str] = None
@@ -34,18 +33,18 @@ class AuthManager:
# Standard headers for login requests # Standard headers for login requests
self.headers = { self.headers = {
'accept': 'application/json, text/plain, */*', "accept": "application/json, text/plain, */*",
'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8,ro;q=0.7', "accept-language": "en-GB,en-US;q=0.9,en;q=0.8,ro;q=0.7",
'content-type': 'application/json;charset=UTF-8', "content-type": "application/json;charset=UTF-8",
'origin': 'https://www.parentzone.me', "origin": "https://www.parentzone.me",
'priority': 'u=1, i', "priority": "u=1, i",
'sec-ch-ua': '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"', "sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
'sec-ch-ua-mobile': '?0', "sec-ch-ua-mobile": "?0",
'sec-ch-ua-platform': '"macOS"', "sec-ch-ua-platform": '"macOS"',
'sec-fetch-dest': 'empty', "sec-fetch-dest": "empty",
'sec-fetch-mode': 'cors', "sec-fetch-mode": "cors",
'sec-fetch-site': 'same-site', "sec-fetch-site": "same-site",
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
} }
async def login(self, email: str, password: str) -> bool: async def login(self, email: str, password: str) -> bool:
@@ -64,18 +63,13 @@ class AuthManager:
self.logger.info(f"Attempting login for {email}") self.logger.info(f"Attempting login for {email}")
# Step 1: Login to get user accounts # Step 1: Login to get user accounts
login_data = { login_data = {"email": email, "password": password}
"email": email,
"password": password
}
timeout = aiohttp.ClientTimeout(total=30) timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
try: try:
async with session.post( async with session.post(
self.login_url, self.login_url, headers=self.headers, json=login_data
headers=self.headers,
json=login_data
) as response: ) as response:
self.logger.info(f"Login response status: {response.status}") self.logger.info(f"Login response status: {response.status}")
@@ -89,20 +83,26 @@ class AuthManager:
if isinstance(data, list) and len(data) > 0: if isinstance(data, list) and len(data) > 0:
# Use the first account # Use the first account
first_account = data[0] first_account = data[0]
self.user_id = first_account.get('id') self.user_id = first_account.get("id")
self.user_name = first_account.get('name') self.user_name = first_account.get("name")
self.provider_name = first_account.get('providerName') self.provider_name = first_account.get("providerName")
self.logger.info(f"Selected account: {self.user_name} at {self.provider_name} (ID: {self.user_id})") self.logger.info(
f"Selected account: {self.user_name} at {self.provider_name} (ID: {self.user_id})"
)
# Step 2: Create session with the account ID # Step 2: Create session with the account ID
return await self._create_session(password) return await self._create_session(password)
else: else:
self.logger.error(f"Unexpected login response format: {data}") self.logger.error(
f"Unexpected login response format: {data}"
)
return False return False
else: else:
error_text = await response.text() error_text = await response.text()
self.logger.error(f"Login failed with status {response.status}: {error_text}") self.logger.error(
f"Login failed with status {response.status}: {error_text}"
)
return False return False
except Exception as e: except Exception as e:
@@ -125,24 +125,21 @@ class AuthManager:
self.logger.info(f"Creating session for user ID: {self.user_id}") self.logger.info(f"Creating session for user ID: {self.user_id}")
session_data = { session_data = {"id": self.user_id, "password": password}
"id": self.user_id,
"password": password
}
# Add x-api-product header for session creation # Add x-api-product header for session creation
session_headers = self.headers.copy() session_headers = self.headers.copy()
session_headers['x-api-product'] = 'iConnect' session_headers["x-api-product"] = "iConnect"
timeout = aiohttp.ClientTimeout(total=30) timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
try: try:
async with session.post( async with session.post(
self.create_session_url, self.create_session_url, headers=session_headers, json=session_data
headers=session_headers,
json=session_data
) as response: ) as response:
self.logger.info(f"Create session response status: {response.status}") self.logger.info(
f"Create session response status: {response.status}"
)
if response.status == 200: if response.status == 200:
data = await response.json() data = await response.json()
@@ -150,16 +147,20 @@ class AuthManager:
self.logger.debug(f"Session response data: {data}") self.logger.debug(f"Session response data: {data}")
# Extract API key from response # Extract API key from response
if isinstance(data, dict) and 'key' in data: if isinstance(data, dict) and "key" in data:
self.api_key = data['key'] self.api_key = data["key"]
self.logger.info("API key obtained successfully") self.logger.info("API key obtained successfully")
return True return True
else: else:
self.logger.error(f"No 'key' field in session response: {data}") self.logger.error(
f"No 'key' field in session response: {data}"
)
return False return False
else: else:
error_text = await response.text() error_text = await response.text()
self.logger.error(f"Session creation failed with status {response.status}: {error_text}") self.logger.error(
f"Session creation failed with status {response.status}: {error_text}"
)
return False return False
except Exception as e: except Exception as e:
@@ -177,8 +178,8 @@ class AuthManager:
if self.api_key: if self.api_key:
# Use x-api-key header for authenticated requests # Use x-api-key header for authenticated requests
headers['x-api-key'] = self.api_key headers["x-api-key"] = self.api_key
headers['x-api-product'] = 'iConnect' headers["x-api-product"] = "iConnect"
return headers return headers
@@ -216,7 +217,11 @@ async def test_login():
print("✅ Login successful!") print("✅ Login successful!")
print(f"User: {auth_manager.user_name} at {auth_manager.provider_name}") print(f"User: {auth_manager.user_name} at {auth_manager.provider_name}")
print(f"User ID: {auth_manager.user_id}") print(f"User ID: {auth_manager.user_id}")
print(f"API Key: {auth_manager.api_key[:20]}..." if auth_manager.api_key else "No API key found") print(
f"API Key: {auth_manager.api_key[:20]}..."
if auth_manager.api_key
else "No API key found"
)
# Test getting auth headers # Test getting auth headers
headers = auth_manager.get_auth_headers() headers = auth_manager.get_auth_headers()

View File

@@ -12,17 +12,16 @@ Usage:
import argparse import argparse
import asyncio import asyncio
import aiohttp
import aiofiles
import os
import json
import logging import logging
from pathlib import Path import os
from urllib.parse import urljoin, urlparse
from typing import List, Dict, Any, Optional
import time import time
from pathlib import Path
from typing import Any, Dict, List
from urllib.parse import urljoin, urlparse
import aiofiles
import aiohttp
from tqdm import tqdm from tqdm import tqdm
import hashlib
# Import the auth manager and asset tracker # Import the auth manager and asset tracker
try: try:
@@ -341,8 +340,8 @@ class ImageDownloader:
# Set file modification time to match the updated timestamp # Set file modification time to match the updated timestamp
if "updated" in asset: if "updated" in asset:
try: try:
from datetime import datetime
import os import os
from datetime import datetime
# Parse the ISO timestamp # Parse the ISO timestamp
updated_time = datetime.fromisoformat( updated_time = datetime.fromisoformat(

View File

@@ -8,16 +8,16 @@ and generates a comprehensive markup file containing all the snapshot informatio
import argparse import argparse
import asyncio import asyncio
import aiohttp import html
import json import json
import logging import logging
import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any, Optional from typing import Any, Dict, List, Optional
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode
import html
import aiofiles import aiofiles
import aiohttp
# Import the auth manager # Import the auth manager
try: try:
@@ -132,12 +132,11 @@ class SnapshotDownloader:
async def fetch_snapshots_page( async def fetch_snapshots_page(
self, self,
session: aiohttp.ClientSession, session: aiohttp.ClientSession,
type_ids: List[int] = [15], type_ids: list[int] = [15],
date_from: str = "2021-10-18", date_from: str = "2021-10-18",
date_to: str = None, date_to: str = "",
cursor: str = None, cursor: str = None,
per_page: int = 100, ) -> dict[str, Any]:
) -> Dict[str, Any]:
""" """
Fetch a single page of snapshots from the API using cursor-based pagination. Fetch a single page of snapshots from the API using cursor-based pagination.
@@ -147,12 +146,11 @@ class SnapshotDownloader:
date_from: Start date in YYYY-MM-DD format date_from: Start date in YYYY-MM-DD format
date_to: End date in YYYY-MM-DD format date_to: End date in YYYY-MM-DD format
cursor: Cursor for pagination (None for first page) cursor: Cursor for pagination (None for first page)
per_page: Number of items per page
Returns: Returns:
Dictionary containing the API response Dictionary containing the API response
""" """
if date_to is None: if date_to == "":
date_to = datetime.now().strftime("%Y-%m-%d") date_to = datetime.now().strftime("%Y-%m-%d")
# Build query parameters # Build query parameters
@@ -167,7 +165,7 @@ class SnapshotDownloader:
# Add type IDs - API expects typeIDs[]=15 format # Add type IDs - API expects typeIDs[]=15 format
for type_id in type_ids: for type_id in type_ids:
params[f"typeIDs[]"] = type_id params["typeIDs[]"] = type_id
# Build URL with parameters # Build URL with parameters
query_string = urlencode(params, doseq=True) query_string = urlencode(params, doseq=True)
@@ -198,13 +196,12 @@ class SnapshotDownloader:
if ( if (
len(data.get("posts", [])) <= 3 len(data.get("posts", [])) <= 3
): # Only print full data if few posts ): # Only print full data if few posts
print(f"Full Response Data:") print("Full Response Data:")
print(json.dumps(data, indent=2, default=str)) print(json.dumps(data, indent=2, default=str))
print("=" * 50) print("=" * 50)
# The API returns snapshots in 'posts' field # The API returns snapshots in 'posts' field
snapshots = data.get("posts", []) snapshots = data.get("posts", [])
cursor_value = data.get("cursor")
page_info = f"cursor: {cursor[:20]}..." if cursor else "first page" page_info = f"cursor: {cursor[:20]}..." if cursor else "first page"
self.logger.info(f"Retrieved {len(snapshots)} snapshots ({page_info})") self.logger.info(f"Retrieved {len(snapshots)} snapshots ({page_info})")
@@ -394,11 +391,11 @@ class SnapshotDownloader:
image_name = html.escape(image.get("fileName", "Image")) image_name = html.escape(image.get("fileName", "Image"))
if local_path: if local_path:
media_html += f'<div class="image-item">\n' media_html += '<div class="image-item">\n'
media_html += f' <img src="{local_path}" alt="{image_name}" loading="lazy">\n' media_html += f' <img src="{local_path}" alt="{image_name}" loading="lazy">\n'
media_html += f' <p class="image-caption">{image_name}</p>\n' media_html += f' <p class="image-caption">{image_name}</p>\n'
media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n' media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n'
media_html += f"</div>\n" media_html += "</div>\n"
else: else:
# Fallback to API URL if download failed # Fallback to API URL if download failed
image_url = ( image_url = (
@@ -407,13 +404,13 @@ class SnapshotDownloader:
else "" else ""
) )
if image_url: if image_url:
media_html += f'<div class="image-item">\n' media_html += '<div class="image-item">\n'
media_html += f' <img src="{image_url}" alt="{image_name}" loading="lazy">\n' media_html += f' <img src="{image_url}" alt="{image_name}" loading="lazy">\n'
media_html += ( media_html += (
f' <p class="image-caption">{image_name} (online)</p>\n' f' <p class="image-caption">{image_name} (online)</p>\n'
) )
media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n' media_html += f' <p class="image-meta">Updated: {self.format_date(image.get("updated", ""))}</p>\n'
media_html += f"</div>\n" media_html += "</div>\n"
media_html += "</div>\n</div>\n" media_html += "</div>\n</div>\n"
@@ -469,11 +466,11 @@ class SnapshotDownloader:
if value: if value:
if isinstance(value, list): if isinstance(value, list):
value = ", ".join(str(v) for v in value) value = ", ".join(str(v) for v in value)
metadata_html += f'<div class="metadata-item">\n' metadata_html += '<div class="metadata-item">\n'
metadata_html += ( metadata_html += (
f" <strong>{label}:</strong> {html.escape(str(value))}\n" f" <strong>{label}:</strong> {html.escape(str(value))}\n"
) )
metadata_html += f"</div>\n" metadata_html += "</div>\n"
# Raw JSON data (collapsed by default) # Raw JSON data (collapsed by default)
metadata_html += '<details class="raw-data">\n' metadata_html += '<details class="raw-data">\n'
@@ -1230,7 +1227,7 @@ Examples:
if html_file: if html_file:
print(f"\n✅ Success! Snapshots downloaded and saved to: {html_file}") print(f"\n✅ Success! Snapshots downloaded and saved to: {html_file}")
print(f"📁 Open the file in your browser to view the snapshots") print("📁 Open the file in your browser to view the snapshots")
else: else:
print("⚠️ No snapshots were found for the specified period") print("⚠️ No snapshots were found for the specified period")

View File

@@ -222,7 +222,6 @@ class SnapshotDownloaderTester:
date_from="2024-01-01", date_from="2024-01-01",
date_to="2024-01-31", date_to="2024-01-31",
page=1, page=1,
per_page=100,
) )
except Exception as e: except Exception as e:
# Expected - we just want to capture the URL # Expected - we just want to capture the URL
@@ -339,7 +338,7 @@ class SnapshotDownloaderTester:
print(" ✅ HTML file created") print(" ✅ HTML file created")
# Check file content # Check file content
with open(html_file, 'r', encoding='utf-8') as f: with open(html_file, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
if "<!DOCTYPE html>" in content: if "<!DOCTYPE html>" in content:
@@ -514,9 +513,7 @@ class SnapshotDownloaderTester:
# Override the fetch_snapshots_page method to use our mock # Override the fetch_snapshots_page method to use our mock
original_method = downloader.fetch_snapshots_page original_method = downloader.fetch_snapshots_page
async def mock_fetch_page( async def mock_fetch_page(session, type_ids, date_from, date_to, page):
session, type_ids, date_from, date_to, page, per_page
):
response_data = mock_session.pages[page - 1] response_data = mock_session.pages[page - 1]
mock_session.call_count += 1 mock_session.call_count += 1
downloader.stats["pages_fetched"] += 1 downloader.stats["pages_fetched"] += 1
@@ -601,6 +598,7 @@ class SnapshotDownloaderTester:
except Exception as e: except Exception as e:
print(f"\n❌ TEST SUITE FAILED: {e}") print(f"\n❌ TEST SUITE FAILED: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return False return False
@@ -660,7 +658,7 @@ def main():
# Setup logging # Setup logging
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
) )
tester = SnapshotDownloaderTester() tester = SnapshotDownloaderTester()