Fix errors and bugs in the given code, without changing its intended functionality. ONLY return the fixed code and nothing else. Enclose the entire code in a single code block.



import argparse
import concurrent.futures
import logging
import os
import queue
from collections import defaultdict

import boto3
import requests
from botocore.exceptions import ClientError
from bs4 import BeautifulSoup

# Configure logging
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


class PatentFileDownloader:
    def __init__(self, base_url, download_dir, s3_bucket):
        """
        Initialize the downloader with configuration parameters.

        Args:
            base_url (str): Base URL to scrape download links from.
            download_dir (str): Local directory to download files to.
            s3_bucket (str): S3 bucket to upload files to.
        """
        self.base_url = base_url
        self.download_dir = download_dir
        self.s3_bucket = s3_bucket

        # Extract year from base URL assuming it's the last part of the URL
        self._year = self.base_url.split("/")[-2]

        # Create download directory if it doesn't exist
        os.makedirs(download_dir, exist_ok=True)

        # S3 client
        self.s3_client = boto3.client("s3")

        # Queues for managing downloads and uploads
        self.download_queue = queue.Queue()
        self.upload_queue = queue.Queue()

    def get_download_links(self):
        """
        Scrape download links from the base URL.

        Returns:
            list: List of download links.
        """
        try:
            # Fetch the page content
            response = requests.get(self.base_url)
            response.raise_for_status()

            # Parse HTML to find download links
            soup = BeautifulSoup(response.text, "html.parser")

            # Find all links ending with .zip
            links = defaultdict(list)

            for link in soup.find_all("a", href=True):
                if link["href"].endswith(".zip"):
                    link_url = self.base_url + link["href"]
                    # extract the datestring from the link

                    datestring = link_url.split("/ipa")[-1].split(".")[0].split("_")[0]

                    links[datestring].append(link_url)

            for date, revisions in links.items():
                # keep only the latest file
                if len(revisions) > 1:
                    revisions = sorted(revisions)
                    links[date] = [revisions[-1]]

            # all lists should be length 1
            links = [value[0] for value in links.values()]
            for link in links:
                logger.info(f"Found download link: {link}")

            return links

        except requests.RequestException as e:
            logger.error(f"Error fetching download links: {e}")
            return []

    def download_file(self, url):
        """
        Download a file from the given URL.

        Args:
            url (str): URL of the file to download.

        Returns:
            str: Path to the downloaded file or None.
        """
        try:
            # Extract filename from URL
            filename = os.path.basename(url)
            filepath = os.path.join(self.download_dir, filename)

            # Stream the download
            with requests.get(url, stream=True) as response:
                response.raise_for_status()
                with open(filepath, "wb") as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)

            logger.info(f"Downloaded: {filename}")
            return filepath

        except requests.RequestException as e:
            logger.error(f"Error downloading {url}: {e}")
            return None

    def upload_to_s3(self, filepath):
        """
        Upload a file to S3.

        Args:
            filepath (str): Local path of the file to upload.

        Returns:
            bool: Boolean indicating success.
        """
        try:
            filename = os.path.basename(filepath)

            # Create folder in S3 for the year if it doesn't exist
            folder = f"{self._year}/"
            if "Contents" not in self.s3_client.list_objects_v2(
                Bucket=self.s3_bucket, Prefix=folder
            ):
                logger.info(f"Creating folder: {folder}")
                self.s3_client.put_object(Bucket=self.s3_bucket, Key=folder)

            # Upload file to the appropriate folder
            self.s3_client.upload_file(
                filepath,
                self.s3_bucket,
                f"{folder}{filename}",
                ExtraArgs={"ACL": "private"},  # or 'public-read' if needed
            )
            logger.info(f"Uploaded: {filename} to {folder}")

            # Delete the file from local directory after successful upload
            os.remove(filepath)
            logger.info(f"Deleted local file: {filepath}")

            return True

        except ClientError as e:
            logger.error(f"Error uploading {filepath} to S3: {e}")
            return False

    def process_downloads(self):
        """
        Process the download queue and download files.
        """
        while True:
            url = self.download_queue.get()
            if url is None:
                break
            filepath = self.download_file(url)
            if filepath:
                self.upload_queue.put(filepath)
            self.download_queue.task_done()

    def process_uploads(self):
        """
        Process the upload queue and upload files to S3.
        """
        while True:
            filepath = self.upload_queue.get()
            if filepath is None:
                break
            self.upload_to_s3(filepath)
            self.upload_queue.task_done()

    def run(self):
        """
        Run the downloader to fetch, download, and upload files.
        """
        # Get download links
        links = self.get_download_links()
        if not links:
            logger.info("No download links found.")
            return

        # Start download and upload threads
        with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
            download_future = executor.submit(self.process_downloads)
            upload_future = executor.submit(self.process_uploads)

            # Add links to download queue
            for link in links:
                self.download_queue.put(link)

            # Wait for all downloads to finish
            self.download_queue.join()

            # After downloads are done, thread to exit
            self.download_queue.put(None)

            # Wait for the upload thread to finish
            self.upload_queue.join()

            self.upload_queue.put(None)

            # Ensure that both threads are completed
            download_future.result()
            upload_future.result()


def main():
    parser = argparse.ArgumentParser(description="Download and upload patent files.")
    parser.add_argument("--min_year", type=int, default=2003, help="Minimum year")
    parser.add_argument("--max_year", type=int, default=2025, help="Maximum year")
    parser.add_argument(
        "--type",
        type=str,
        default="application",
        help="Type of patent",
        choices=["grant", "application"],
    )
    parser.add_argument(
        "--bucket",
        type=str,
        default="usptoapplications",
        help="S3 bucket",
        choices=["usptoapplications", "usptogrants"],
    )

    args = parser.parse_args()

    # Configuration
    BASE_URL = f"https://bulkdata.uspto.gov/data/patent/{args.type}/redbook/fulltext/"
    DOWNLOAD_DIR = "downloaded_files"
    S3_BUCKET = args.bucket

    for year in range(args.min_year, args.max_year + 1):
        url = f"{BASE_URL}{year}/"

        # Create and run downloader
        downloader = PatentFileDownloader(url, DOWNLOAD_DIR, S3_BUCKET)
        downloader.run()


if __name__ == "__main__":
    main()