From edd1e130e67426fc2be66b13c72041a81b57df82 Mon Sep 17 00:00:00 2001 From: acidvegas Date: Fri, 21 Mar 2025 01:33:19 -0400 Subject: [PATCH] Added verbosity --- czds/__init__.py | 2 +- czds/__main__.py | 1 + czds/client.py | 64 ++++++++++++++++++++++++++++++++++++------------ setup.py | 2 +- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/czds/__init__.py b/czds/__init__.py index 3050599..a78708f 100644 --- a/czds/__init__.py +++ b/czds/__init__.py @@ -5,7 +5,7 @@ from .client import CZDS -__version__ = '1.2.1' +__version__ = '1.2.2' __author__ = 'acidvegas' __email__ = 'acid.vegas@acid.vegas' __github__ = 'https://github.com/acidvegas/czds' \ No newline at end of file diff --git a/czds/__main__.py b/czds/__main__.py index 3ab09ab..f056a80 100644 --- a/czds/__main__.py +++ b/czds/__main__.py @@ -78,5 +78,6 @@ def cli_entry(): return asyncio.run(main()) + if __name__ == '__main__': asyncio.run(main()) \ No newline at end of file diff --git a/czds/client.py b/czds/client.py index 123f8d0..45a0a70 100644 --- a/czds/client.py +++ b/czds/client.py @@ -3,8 +3,10 @@ # czds/client.py import asyncio -import os import gzip +import logging +import os + try: import aiohttp @@ -17,6 +19,13 @@ except ImportError: raise ImportError('missing aiofiles library (pip install aiofiles)') +# Configure logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) + + class CZDS: '''Class for the ICANN Centralized Zones Data Service''' @@ -32,6 +41,7 @@ class CZDS: self.password = password self.headers = None # Store the authorization header for reuse self.session = None # Store the client session for reuse + logging.info('Initialized CZDS client') async def __aenter__(self): @@ -39,7 +49,7 @@ class CZDS: self.session = aiohttp.ClientSession() self.headers = {'Authorization': f'Bearer {await self.authenticate()}'} - + logging.debug('Entered async context') return self @@ -48,6 +58,7 @@ class CZDS: if self.session: await self.session.close() + logging.debug('Closed aiohttp session') async def authenticate(self) -> str: @@ -55,27 +66,36 @@ class CZDS: try: data = {'username': self.username, 'password': self.password} + logging.info('Authenticating with ICANN API') async with self.session.post('https://account-api.icann.org/api/authenticate', json=data) as response: if response.status != 200: - raise Exception(f'Authentication failed: {response.status} {await response.text()}') + error_msg = f'Authentication failed: {response.status} {await response.text()}' + logging.error(error_msg) + raise Exception(error_msg) result = await response.json() - + logging.info('Successfully authenticated with ICANN API') return result['accessToken'] except Exception as e: - raise Exception(f'Failed to authenticate with ICANN API: {e}') + error_msg = f'Failed to authenticate with ICANN API: {e}' + logging.error(error_msg) + raise Exception(error_msg) async def fetch_zone_links(self) -> list: '''Fetch the list of zone files available for download''' - + logging.info('Fetching zone links') async with self.session.get('https://czds-api.icann.org/czds/downloads/links', headers=self.headers) as response: if response.status != 200: - raise Exception(f'Failed to fetch zone links: {response.status} {await response.text()}') + error_msg = f'Failed to fetch zone links: {response.status} {await response.text()}' + logging.error(error_msg) + raise Exception(error_msg) - return await response.json() + links = await response.json() + logging.info(f'Successfully fetched {len(links)} zone links') + return links async def get_report(self, filepath: str = None, scrub: bool = True, format: str = 'csv') -> str | dict: @@ -87,20 +107,24 @@ class CZDS: :param format: Output format ('csv' or 'json') :return: Report content as CSV string or JSON dict ''' - + logging.info('Downloading zone stats report') async with self.session.get('https://czds-api.icann.org/czds/requests/report', headers=self.headers) as response: if response.status != 200: - raise Exception(f'Failed to download the zone stats report: {response.status} {await response.text()}') + error_msg = f'Failed to download the zone stats report: {response.status} {await response.text()}' + logging.error(error_msg) + raise Exception(error_msg) content = await response.text() if scrub: content = content.replace(self.username, 'nobody@no.name') + logging.debug('Scrubbed username from report') if format.lower() == 'json': rows = [row.split(',') for row in content.strip().split('\n')] header = rows[0] content = [dict(zip(header, row)) for row in rows[1:]] + logging.debug('Converted report to JSON format') if filepath: async with aiofiles.open(filepath, 'w') as file: @@ -109,9 +133,10 @@ class CZDS: await file.write(json.dumps(content, indent=4)) else: await file.write(content) + logging.info(f'Saved report to {filepath}') return content - + async def gzip_decompress(self, filepath: str, cleanup: bool = True): ''' @@ -120,7 +145,7 @@ class CZDS: :param filepath: Path to the gzip file :param cleanup: Whether to remove the original gzip file after decompression ''' - + logging.debug(f'Decompressing {filepath}') output_path = filepath[:-3] # Remove .gz extension async with aiofiles.open(filepath, 'rb') as f_in: @@ -131,6 +156,7 @@ class CZDS: if cleanup: os.remove(filepath) + logging.debug(f'Removed original gzip file: {filepath}') async def download_zone(self, url: str, output_directory: str, decompress: bool = False, cleanup: bool = True, semaphore: asyncio.Semaphore = None): @@ -143,14 +169,18 @@ class CZDS: :param cleanup: Whether to remove the original gzip file after decompression :param semaphore: Optional semaphore for controlling concurrency ''' - async def _download(): + logging.debug(f'Downloading zone file from {url}') async with self.session.get(url, headers=self.headers) as response: if response.status != 200: - raise Exception(f'Failed to download {url}: {response.status} {await response.text()}') + error_msg = f'Failed to download {url}: {response.status} {await response.text()}' + logging.error(error_msg) + raise Exception(error_msg) if not (content_disposition := response.headers.get('Content-Disposition')): - raise ValueError('Missing Content-Disposition header') + error_msg = 'Missing Content-Disposition header' + logging.error(error_msg) + raise ValueError(error_msg) filename = content_disposition.split('filename=')[-1].strip('"') filepath = os.path.join(output_directory, filename) @@ -161,6 +191,7 @@ class CZDS: if not chunk: break await file.write(chunk) + logging.info(f'Successfully downloaded {filename}') if decompress: await self.gzip_decompress(filepath, cleanup) @@ -184,11 +215,12 @@ class CZDS: :param decompress: Whether to decompress the gzip files after download :param cleanup: Whether to remove the original gzip files after decompression ''' - os.makedirs(output_directory, exist_ok=True) + logging.info(f'Starting concurrent download of zones with concurrency={concurrency}') semaphore = asyncio.Semaphore(concurrency) zone_links = await self.fetch_zone_links() tasks = [self.download_zone(url, output_directory, decompress, cleanup, semaphore) for url in zone_links] await asyncio.gather(*tasks) + logging.info('Completed downloading all zone files') diff --git a/setup.py b/setup.py index 09b6841..2bb0dd4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open('README.md', 'r', encoding='utf-8') as fh: setup( name='czds-api', - version='1.2.1', + version='1.2.2', author='acidvegas', author_email='acid.vegas@acid.vegas', description='ICANN API for the Centralized Zones Data Service',