Repository: abhi-kr-2100/Lingetic
Pull request #202: Add script to upload to R2
Author: login: abhi-kr-2100
Created: 2025-04-19T16:33:05Z | Merged: 2025-04-19T20:07:42Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a command-line tool for uploading files or directories to Cloudflare R2 storage with options for recursion, custom prefixes, and detailed logging.

- **Chores**
  - Added a new dependency requirement for `boto3` (version 1.37.37 or higher).

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml
index 28ff383c..5edf353d 100644
--- a/scripts/pyproject.toml
+++ b/scripts/pyproject.toml
@@ -15,6 +15,7 @@ dependencies = [
     "aiofiles>=24.1.0",
     "pdf2image>=1.17.0",
     "gtts>=2.5.4",
+    "boto3>=1.37.37",
 ]
 
 [tool.ruff]
diff --git a/scripts/scripts/__init__.py b/scripts/scripts/__init__.py
index a86b66d8..b431d9a4 100644
--- a/scripts/scripts/__init__.py
+++ b/scripts/scripts/__init__.py
@@ -7,6 +7,7 @@
 from . import images_to_sentence_jsons
 from . import pdf_to_images
 from . import tts
+from . import upload_to_r2
 
 __all__ = [
     "hello",
@@ -18,4 +19,5 @@
     "images_to_sentence_jsons",
     "pdf_to_images",
     "tts",
+    "upload_to_r2",
 ]
diff --git a/scripts/scripts/upload_to_r2.py b/scripts/scripts/upload_to_r2.py
new file mode 100644
index 00000000..d9a1b991
--- /dev/null
+++ b/scripts/scripts/upload_to_r2.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+
+    client = create_r2_client(account_id, access_key_id, secret_access_key)
+
+    results = process_directory(
+        client, directory, bucket_name, prefix, recursive
+    )
+
+    logger.info(
+        f"Upload complete: {len(results['successful'])} successful, {len(results['failed'])} failed"
+    )
+
+    if results["failed"]:
+        logger.warning("Failed uploads:")
+        for filepath in results["failed"]:
+            logger.warning(f"  - {filepath}")
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """
+    Create and configure the argument parser for the script.
+
+    Returns:
+        An ArgumentParser object configured with the script's arguments.
+    """
+    parser = argparse.ArgumentParser(
+        description="Upload files from a directory to Cloudflare R2 storage."
+    )
+    parser.add_argument(
+        "-d",
+        "--directory",
+        required=True,
+        help="Directory containing files to upload.",
+    )
+    parser.add_argument(
+        "-b",
+        "--bucket-name",
+        default="lingetic-speech-recordings",
+        help="Name of the R2 bucket.",
+    )
+    parser.add_argument(
+        "-a", "--account-id", required=True, help="Cloudflare account ID."
+    )
+    parser.add_argument(
+        "-k", "--access-key-id", required=True, help="R2 access key ID."
+    )
+    parser.add_argument(
+        "-s", "--secret-access-key", required=True, help="R2 secret access key."
+    )
+    parser.add_argument(
+        "-p", "--prefix", help="Optional prefix for object keys in R2."
+    )
+    parser.add_argument(
+        "-r",
+        "--recursive",
+        action="store_true",
+        help="Recursively process subdirectories.",
+    )
+    parser.add_argument(
+        "-v", "--verbose", action="store_true", help="Enable verbose logging."
+    )
+
+    return parser
+
+
+if __name__ == "__main__":
+    parser = get_parser()
+    args = parser.parse_args()
+
+    main(
+        directory=args.directory,
+        bucket_name=args.bucket_name,
+        account_id=args.account_id,
+        access_key_id=args.access_key_id,
+        secret_access_key=args.secret_access_key,
+        prefix=args.prefix,
+        recursive=args.recursive,
+        verbose=args.verbose,
+    )
diff --git a/scripts/uv.lock b/scripts/uv.lock
index 8408838d..788e123e 100644
--- a/scripts/uv.lock
+++ b/scripts/uv.lock
@@ -34,6 +34,34 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
 ]
 
+[[package]]
+name = "boto3"
+version = "1.37.37"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "botocore" },
+    { name = "jmespath" },
+    { name = "s3transfer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/8c/2ca661db6c9e591d9dc46149b43a91385283c852436ccba62e199643e196/boto3-1.37.37.tar.gz", hash = "sha256:752d31105a45e3e01c8c68471db14ae439990b75a35e72b591ca528e2575b28f", size = 111666 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e4/5f/032d93e74949222ffbfbc3270f29a3ee423fe648de8a31c49cce0cbb0a09/boto3-1.37.37-py3-none-any.whl", hash = "sha256:d125cb11e22817f7a2581bade4bf7b75247b401888890239ceb5d3e902ccaf38", size = 139917 },
+]
+
+[[package]]
+name = "botocore"
+version = "1.37.37"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "jmespath" },
+    { name = "python-dateutil" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/d0/70969515e3ae8ff0fcccf22827d5d131bc7b8729331127415cf8f2861d63/botocore-1.37.37.tar.gz", hash = "sha256:3eadde6fed95c4cb469cc39d1c3558528b7fa76d23e7e16d4bddc77250431a64", size = 13828530 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fe/17/602915b29cb695e1e66f65e33b1026f1534e49975d99ea4e32e58d963542/botocore-1.37.37-py3-none-any.whl", hash = "sha256:eb730ff978f47c02f0c8ed07bccdc0db6d8fa098ed32ac31bee1da0e9be480d1", size = 13495584 },
+]
+
 [[package]]
 name = "cachetools"
 version = "5.5.2"
@@ -199,6 +227,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
 ]
 
+[[package]]
+name = "jmespath"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 },
+]
+
 [[package]]
 name = "ollama"
 version = "0.4.7"
@@ -367,6 +404,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
 ]
 
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
 [[package]]
 name = "requests"
 version = "2.32.3"
@@ -419,12 +468,25 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
 ]
 
+[[package]]
+name = "s3transfer"
+version = "0.11.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "botocore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/2b/5c9562795c2eb2b5f63536961754760c25bf0f34af93d36aa28dea2fb303/s3transfer-0.11.5.tar.gz", hash = "sha256:8c8aad92784779ab8688a61aefff3e28e9ebdce43142808eaa3f0b0f402f68b7", size = 149107 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/45/39/13402e323666d17850eca87e4cd6ecfcf9fd7809cac9efdcce10272fc29d/s3transfer-0.11.5-py3-none-any.whl", hash = "sha256:757af0f2ac150d3c75bc4177a32355c3862a98d20447b69a0161812992fe0bd4", size = 84782 },
+]
+
 [[package]]
 name = "scripts"
 version = "0.1.0"
 source = { virtual = "." }
 dependencies = [
     { name = "aiofiles" },
+    { name = "boto3" },
     { name = "google-genai" },
     { name = "gtts" },
     { name = "ollama" },
@@ -439,6 +501,7 @@ dependencies = [
 [package.metadata]
 requires-dist = [
     { name = "aiofiles", specifier = ">=24.1.0" },
+    { name = "boto3", specifier = ">=1.37.37" },
     { name = "google-genai", specifier = ">=1.8.0" },
     { name = "gtts", specifier = ">=2.5.4" },
     { name = "ollama", specifier = ">=0.4.7" },
@@ -450,6 +513,15 @@ requires-dist = [
     { name = "uuid", specifier = ">=1.30" },
 ]
 
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
+]
+
 [[package]]
 name = "sniffio"
 version = "1.3.1"


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @abhi-kr-2100, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a new script, `upload_to_r2.py`, designed to upload files from a specified directory to Cloudflare R2 storage. It includes functionality for recursive directory processing, content type detection, and error handling. The PR also updates `pyproject.toml` to include `boto3` as a dependency and modifies `scripts/__init__.py` to include the new `upload_to_r2` module. Finally, the `uv.lock` file is updated to reflect the new dependencies.

### Highlights
* **New Script: upload_to_r2.py**: Adds a script to upload files to Cloudflare R2 storage, handling directory traversal (recursive or not), content type detection, and error logging.
* **Dependency: boto3**: Includes `boto3` as a dependency in `pyproject.toml` to interact with Cloudflare R2 via the S3 API.
* **Module Integration**: Integrates the new `upload_to_r2` module into the `scripts` package by updating `scripts/__init__.py`.
* **Dependency Locking**: Updates `uv.lock` to include `boto3`, `botocore`, `jmespath`, `python-dateutil`, `s3transfer`, and `six` with their specific versions and hashes, ensuring consistent dependency management.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **scripts/pyproject.toml**
  * Added `boto3>=1.37.37` as a dependency.
* **scripts/scripts/__init__.py**
  * Imported `upload_to_r2` module.
  * Added `upload_to_r2` to the `__all__` list.
* **scripts/scripts/upload_to_r2.py**
  * Created a new script to upload files to Cloudflare R2 storage.
  * Implements argument parsing, R2 client creation, file uploading, and directory processing.
  * Includes functions for generating object keys, determining content types, and handling errors.
* **scripts/uv.lock**
  * Added entries for `boto3`, `botocore`, `jmespath`, `python-dateutil`, `s3transfer`, and `six` with their respective versions, sources, dependencies, sdists, and wheels.
  * Updated `scripts` package dependencies to include `boto3`.
  * Updated `scripts` package metadata to require `boto3`.

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> To R2 the files ascend,
> With boto3 as our friend,
> A bucket in the cloud,
> Where data's safe and proud,
> No longer on disk to depend.


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-19T16:33:15Z
user: login: gemini-code-assist[bot]
body: ## Code Review
The pull request introduces a new script, `upload_to_r2.py`, to upload files to Cloudflare R2 storage. The script is well-structured and includes necessary functionalities such as generating object keys, determining content types, creating an R2 client, and uploading files. The addition of boto3 as a dependency is also correctly reflected in `pyproject.toml` and `uv.lock`. Overall, the script seems functional and addresses the intended purpose.

### Summary of Findings
* **Error Handling**: The script includes basic error handling, but could be improved by providing more context in the error messages, and potentially adding retry logic for transient errors.
* **Logging**: Consider adding more detailed logging, especially around the R2 client creation and file upload processes, to aid in debugging and monitoring.
* **Argument Validation**: The script relies on argparse for argument parsing, but could benefit from additional validation to ensure that the provided arguments are valid (e.g., checking if the directory exists and is readable).

### Merge Readiness
The pull request is almost ready for merging. The script is well-structured and includes necessary functionalities. However, addressing the `high` severity issue related to the default bucket name and considering the `medium` severity suggestions for enhanced error handling and logging would improve the script's robustness and usability. I am unable to approve this pull request, and recommend that others review and approve this code before merging. At a minimum, the high severity issue should be addressed before merging.
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-19T16:33:24Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>scripts/scripts/upload_to_r2.py (1)</summary><blockquote>

`1-279`: _🛠️ Refactor suggestion_

**Add missing testing coverage**

The script doesn't include any tests. Consider adding unit tests to verify the functionality and ensure it continues to work as expected when modifications are made.


Since this is a new module, it's important to add tests to ensure it works correctly. Here's a suggestion for a test file structure:

```python
# test_upload_to_r2.py

import unittest
from unittest.mock import patch, MagicMock, mock_open
import os
import boto3
from botocore.exceptions import ClientError

from scripts.upload_to_r2 import (
    generate_object_key,
    get_content_type,
    create_r2_client,
    upload_file,
    process_directory,
)

class TestGenerateObjectKey(unittest.TestCase):
    def test_without_prefix(self):
        self.assertEqual(generate_object_key("/path/to/file.txt"), "file.txt")
    
    def test_with_prefix(self):
        self.assertEqual(generate_object_key("/path/to/file.txt", "prefix"), "prefix/file.txt")
    
    # Additional tests for edge cases

class TestGetContentType(unittest.TestCase):
    def test_known_file_type(self):
        self.assertEqual(get_content_type("test.txt"), "text/plain")
    
    def test_unknown_file_type(self):
        self.assertEqual(get_content_type("noextension"), "application/octet-stream")

class TestCreateR2Client(unittest.TestCase):
    @patch('boto3.client')
    def test_client_creation(self, mock_client):
        create_r2_client("account", "key", "secret")
        mock_client.assert_called_once_with(
            service_name="s3",
            endpoint_url="https://account.r2.cloudflarestorage.com",
            aws_access_key_id="key",
            aws_secret_access_key="secret",
            region_name="auto",
        )

class TestUploadFile(unittest.TestCase):
    @patch('builtins.open', new_callable=mock_open, read_data=b'test data')
    def test_successful_upload(self, mock_file):
        mock_client = MagicMock()
        result = upload_file(mock_client, "test.txt", "bucket", "key")
        self.assertTrue(result)
        mock_client.put_object.assert_called_once()
    
    # Add tests for error handling

class TestProcessDirectory(unittest.TestCase):
    # Tests for directory processing logic
    pass

# Additional test classes for other functions
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3e56f6005bd496a0d2e88b2a8ce4ba2fb8d74bec and 804f6172ffc73dc145184a5986c933b2e358e2da.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `scripts/uv.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `scripts/pyproject.toml` (1 hunks)
* `scripts/scripts/__init__.py` (2 hunks)
* `scripts/scripts/upload_to_r2.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Codacy Static Code Analysis

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>scripts/pyproject.toml (1)</summary>

`18-18`: **Dependency addition looks good.**

The addition of boto3 with a minimum version requirement is appropriate for the new Cloudflare R2 upload functionality.

</details>
<details>
<summary>scripts/scripts/__init__.py (1)</summary>

`10-10`: **Export of upload_to_r2 module looks good.**

Properly exposing the new upload_to_r2 module as part of the package's public API.



Also applies to: 22-22

</details>
<details>
<summary>scripts/scripts/upload_to_r2.py (2)</summary>

`1-11`: **Imports look appropriate**

The script includes all necessary imports for the functionality it provides. The combination of boto3 for R2/S3 interaction, argparse for CLI handling, and logging for operation reporting is appropriate.

---

`40-52`: **Content type detection looks good**

The content type detection function is well-implemented, using Python's mimetypes library with a fallback to the generic binary type.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-19T16:36:20Z

Review comments:
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+
+    client = create_r2_client(account_id, access_key_id, secret_access_key)
+
+    results = process_directory(
+        client, directory, bucket_name, prefix, recursive
+    )
+
+    logger.info(
+        f"Upload complete: {len(results['successful'])} successful, {len(results['failed'])} failed"
+    )
+
+    if results["failed"]:
+        logger.warning("Failed uploads:")
+        for filepath in results["failed"]:
+            logger.warning(f"  - {filepath}")
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """
+    Create and configure the argument parser for the script.
+
+    Returns:
+        An ArgumentParser object configured with the script's arguments.
+    """
+    parser = argparse.ArgumentParser(
+        description="Upload files from a directory to Cloudflare R2 storage."
+    )
+    parser.add_argument(
+        "-d",
+        "--directory",
+        required=True,
+        help="Directory containing files to upload.",
+    )
+    parser.add_argument(
+        "-b",
+        "--bucket-name",
+        default="lingetic-speech-recordings",
+        help="Name of the R2 bucket.",
path: scripts/scripts/upload_to_r2.py
user: login: gemini-code-assist[bot]
body: ![high](https://img.shields.io/badge/priority-high-orange)

Having a default bucket name can be convenient, but it also introduces a risk of accidental uploads to the wrong bucket if the user doesn't explicitly specify one. It might be safer to require the bucket name to be explicitly provided.

If you do want to keep the default, consider adding a warning message if the user doesn't specify the bucket name, reminding them to double-check the configuration.

```python
#        "--bucket-name",
#        default="lingetic-speech-recordings",
#        help="Name of the R2 bucket.",
```
created_at: 2025-04-19T16:33:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
path: scripts/scripts/upload_to_r2.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider including the specific error from `e` in the log message to provide more context for debugging.
For example, you could log `f"Error uploading {filepath}: {type(e).__name__} - {e}"`

```suggestion
        logger.error(f"Error uploading {filepath}: {type(e).__name__} - {e}")
        return False
```
created_at: 2025-04-19T16:33:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
path: scripts/scripts/upload_to_r2.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Similar to the `ClientError` handling, including the exception type and message in the log would be helpful.
For example, you could log `f"Unexpected error uploading {filepath}: {type(e).__name__} - {e}"`

```suggestion
        logger.error(f"Unexpected error uploading {filepath}: {type(e).__name__} - {e}")
        return False
```
created_at: 2025-04-19T16:33:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
path: scripts/scripts/upload_to_r2.py
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Consider adding a default value for the `verbose` argument to avoid having to specify it every time the script is run.

```python
    verbose: bool = False,

```
created_at: 2025-04-19T16:33:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider allowing log level configuration from arguments**

The logging setup is good, but the level is initially set to INFO and only can be changed to DEBUG with the verbose flag. Consider making the log level fully configurable through the CLI arguments for more flexibility.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Enhance error reporting in upload_file function**

The error handling is good, but the function could provide more specific error information to help with debugging. Consider returning more detailed error information instead of just True/False.

```diff
 def upload_file(
     client: boto3.client, filepath: str, bucket_name: str, object_key: str
-) -> bool:
+) -> Dict[str, Any]:
     """
     Upload a file to Cloudflare R2.
 
     Args:
         client: Boto3 S3 client configured for R2.
         filepath: Path to the file to be uploaded.
         bucket_name: Name of the R2 bucket.
         object_key: Key for the object in R2 storage.
 
     Returns:
-        True if the upload was successful, False otherwise.
+        Dictionary with 'success' (bool) and optional 'error' (str) keys.
     """
     try:
         content_type = get_content_type(filepath)
 
         with open(filepath, "rb") as file:
             client.put_object(
                 Bucket=bucket_name,
                 Key=object_key,
                 Body=file,
                 ContentType=content_type,
             )
-        return True
+        return {"success": True}
 
     except ClientError as e:
         logger.error(f"Error uploading {filepath}: {e}")
-        return False
+        return {"success": False, "error": str(e)}
 
     except Exception as e:
         logger.error(f"Unexpected error uploading {filepath}: {e}")
-        return False
+        return {"success": False, "error": f"Unexpected error: {str(e)}"}
```

Then update the process_directory function to handle the new return value:

```python
# In process_directory function:
result = upload_file(client, filepath, bucket_name, object_key)
if result["success"]:
    # Success path
else:
    # Error path, can now include result["error"] in logs
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+
+    client = create_r2_client(account_id, access_key_id, secret_access_key)
+
+    results = process_directory(
+        client, directory, bucket_name, prefix, recursive
+    )
+
+    logger.info(
+        f"Upload complete: {len(results['successful'])} successful, {len(results['failed'])} failed"
+    )
+
+    if results["failed"]:
+        logger.warning("Failed uploads:")
+        for filepath in results["failed"]:
+            logger.warning(f"  - {filepath}")
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider adding a dry-run option**

The main function could benefit from a "dry-run" option that would show what files would be uploaded without actually performing the uploads. This would help users verify their settings before executing a large upload operation.

```diff
 def main(
     directory: str,
     bucket_name: str,
     account_id: str,
     access_key_id: str,
     secret_access_key: str,
     prefix: Optional[str] = None,
     recursive: bool = False,
     verbose: bool = False,
+    dry_run: bool = False,
 ) -> None:
     """
     Main function to upload files from a directory to Cloudflare R2.
 
     Args:
         directory: Directory containing files to upload.
         bucket_name: Name of the R2 bucket.
         account_id: Cloudflare account ID.
         access_key_id: R2 access key ID.
         secret_access_key: R2 secret access key.
         prefix: Optional prefix for object keys.
         recursive: If True, process subdirectories recursively.
         verbose: If True, enable verbose logging.
+        dry_run: If True, only show what would be uploaded.
     """
     if verbose:
         logger.setLevel(logging.DEBUG)
 
+    if dry_run:
+        logger.info("DRY RUN MODE: No files will be uploaded")
+
     client = create_r2_client(account_id, access_key_id, secret_access_key)
 
     results = process_directory(
-        client, directory, bucket_name, prefix, recursive
+        client, directory, bucket_name, prefix, recursive, dry_run
     )
```

And add the dry_run parameter to the process_directory function and update it to skip the actual upload when in dry-run mode.

Don't forget to also update the argument parser to include the dry-run option:

```python
# In get_parser function:
parser.add_argument(
    "--dry-run",
    action="store_true",
    help="Show what would be uploaded without actually uploading.",
)
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+
+    client = create_r2_client(account_id, access_key_id, secret_access_key)
+
+    results = process_directory(
+        client, directory, bucket_name, prefix, recursive
+    )
+
+    logger.info(
+        f"Upload complete: {len(results['successful'])} successful, {len(results['failed'])} failed"
+    )
+
+    if results["failed"]:
+        logger.warning("Failed uploads:")
+        for filepath in results["failed"]:
+            logger.warning(f"  - {filepath}")
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """
+    Create and configure the argument parser for the script.
+
+    Returns:
+        An ArgumentParser object configured with the script's arguments.
+    """
+    parser = argparse.ArgumentParser(
+        description="Upload files from a directory to Cloudflare R2 storage."
+    )
+    parser.add_argument(
+        "-d",
+        "--directory",
+        required=True,
+        help="Directory containing files to upload.",
+    )
+    parser.add_argument(
+        "-b",
+        "--bucket-name",
+        default="lingetic-speech-recordings",
+        help="Name of the R2 bucket.",
+    )
+    parser.add_argument(
+        "-a", "--account-id", required=True, help="Cloudflare account ID."
+    )
+    parser.add_argument(
+        "-k", "--access-key-id", required=True, help="R2 access key ID."
+    )
+    parser.add_argument(
+        "-s", "--secret-access-key", required=True, help="R2 secret access key."
+    )
+    parser.add_argument(
+        "-p", "--prefix", help="Optional prefix for object keys in R2."
+    )
+    parser.add_argument(
+        "-r",
+        "--recursive",
+        action="store_true",
+        help="Recursively process subdirectories.",
+    )
+    parser.add_argument(
+        "-v", "--verbose", action="store_true", help="Enable verbose logging."
+    )
+
+    return parser
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add environment variables support for credentials**

The argument parser requires sensitive information like access keys to be passed via command line, which isn't ideal for security. Consider adding support for environment variables as an alternative way to provide these credentials.

```diff
 def get_parser() -> argparse.ArgumentParser:
     """
     Create and configure the argument parser for the script.
 
     Returns:
         An ArgumentParser object configured with the script's arguments.
     """
     parser = argparse.ArgumentParser(
-        description="Upload files from a directory to Cloudflare R2 storage."
+        description="Upload files from a directory to Cloudflare R2 storage. "
+                    "Credentials can also be provided via environment variables: "
+                    "R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY."
     )
     parser.add_argument(
         "-d",
         "--directory",
         required=True,
         help="Directory containing files to upload.",
     )
     parser.add_argument(
         "-b",
         "--bucket-name",
         default="lingetic-speech-recordings",
         help="Name of the R2 bucket.",
     )
     parser.add_argument(
         "-a", "--account-id", required=True, help="Cloudflare account ID."
     )
     parser.add_argument(
         "-k", "--access-key-id", required=True, help="R2 access key ID."
     )
     parser.add_argument(
         "-s", "--secret-access-key", required=True, help="R2 secret access key."
     )
```

Then, update the main script to check for environment variables if command-line arguments aren't provided:

```python
# In __main__ section:
parser = get_parser()
args = parser.parse_args()

# Check environment variables for credentials if not provided
account_id = args.account_id or os.environ.get('R2_ACCOUNT_ID')
access_key_id = args.access_key_id or os.environ.get('R2_ACCESS_KEY_ID')
secret_access_key = args.secret_access_key or os.environ.get('R2_SECRET_ACCESS_KEY')

# Validate required credentials
if not all([account_id, access_key_id, secret_access_key]):
    missing = []
    if not account_id:
        missing.append("account ID")
    if not access_key_id:
        missing.append("access key ID")
    if not secret_access_key:
        missing.append("secret access key")
    parser.error(f"Missing required credentials: {', '.join(missing)}")

main(
    directory=args.directory,
    bucket_name=args.bucket_name,
    account_id=account_id,
    access_key_id=access_key_id,
    secret_access_key=secret_access_key,
    prefix=args.prefix,
    recursive=args.recursive,
    verbose=args.verbose,
)
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Improve handling of large directories**

The process_directory function reads all files at once, which could be inefficient for large directories. Consider adding a progress indicator or batch processing for better user experience with large uploads.

Also, the error handling when a directory doesn't exist could be improved to return the error rather than exiting the program, allowing for better integration with other code.

```diff
 def process_directory(
     client: boto3.client,
     directory: str,
     bucket_name: str,
     prefix: Optional[str] = None,
     recursive: bool = False,
 ) -> Dict[str, List[Any]]:
     """
     Process a directory and upload files to R2.
 
     Args:
         client: Boto3 S3 client configured for R2.
         directory: Directory containing files to upload.
         bucket_name: Name of the R2 bucket.
         prefix: Optional prefix for object keys.
         recursive: If True, process subdirectories recursively.
 
     Returns:
-        A dictionary with lists of successful and failed uploads.
+        A dictionary with lists of successful and failed uploads, or error info.
     """
     if not os.path.exists(directory):
         logger.error(f"Directory '{directory}' not found")
-        sys.exit(1)
+        return {"error": f"Directory '{directory}' not found"}
 
     if not os.path.isdir(directory):
         logger.error(f"'{directory}' is not a directory")
-        sys.exit(1)
+        return {"error": f"'{directory}' is not a directory"}
 
     results = {"successful": [], "failed": []}
+    total_files = 0
+    
+    # Count total files for progress reporting
+    if os.path.exists(directory) and os.path.isdir(directory):
+        for root, _, files in os.walk(directory):
+            if not recursive and root != directory:
+                continue
+            total_files += len(files)
+        
+        logger.info(f"Found {total_files} files to process")
+
+    processed = 0
 
     for root, dirs, files in os.walk(directory):
         if not recursive and root != directory:
             continue
 
         for filename in files:
+            processed += 1
+            if processed % 10 == 0 or processed == total_files:
+                logger.info(f"Progress: {processed}/{total_files} files ({processed/total_files*100:.1f}%)")
             filepath = os.path.join(root, filename)
 
             # Rest of the function remains the same
```

Also, update the main function to handle the error return from process_directory:

```python
# In main function:
results = process_directory(...)
if "error" in results:
    logger.error(results["error"])
    return
# Continue with the rest of the function
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Object key generation looks good, but consider path normalization**

The object key generation function works well, but it might be beneficial to normalize file paths to ensure consistent keys across different operating systems (e.g., handling backslashes vs. forward slashes).

```diff
 def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
     """
     Generate a unique object key for R2 storage based on the file path.
 
     Args:
         filepath: Path to the file to be uploaded.
         prefix: Optional prefix to add to the object key.
 
     Returns:
         A unique object key for R2 storage.
     """
-    filename = os.path.basename(filepath)
+    # Normalize filepath to handle different OS path formats
+    normalized_path = os.path.normpath(filepath)
+    filename = os.path.basename(normalized_path)
 
     if prefix:
         prefix = prefix.rstrip("/") + "/"
         return f"{prefix}{filename}"
 
     return filename
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
    """
    Generate a unique object key for R2 storage based on the file path.

    Args:
        filepath: Path to the file to be uploaded.
        prefix: Optional prefix to add to the object key.

    Returns:
        A unique object key for R2 storage.
    """
    # Normalize filepath to handle different OS path formats
    normalized_path = os.path.normpath(filepath)
    filename = os.path.basename(normalized_path)

    if prefix:
        prefix = prefix.rstrip("/") + "/"
        return f"{prefix}{filename}"

    return filename
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Path handling could be simplified**

The logic for determining the full_prefix is complex and could be simplified for better maintainability. Consider refactoring this section for clarity.

```diff
-            relative_path = os.path.relpath(filepath, directory)
-            dir_part = os.path.dirname(relative_path)
-
-            if prefix and dir_part and recursive:
-                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
-            elif prefix:
-                full_prefix = prefix
-            elif dir_part and recursive:
-                full_prefix = dir_part
-            else:
-                full_prefix = None
+            # Calculate relative path from the base directory
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+            
+            # Start with the provided prefix if any
+            full_prefix = prefix
+            
+            # If we're handling recursively and the file is in a subdirectory
+            if recursive and dir_part:
+                if full_prefix:
+                    full_prefix = f"{full_prefix.rstrip('/')}/{dir_part}"
+                else:
+                    full_prefix = dir_part
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            # Calculate relative path from the base directory
            relative_path = os.path.relpath(filepath, directory)
            dir_part = os.path.dirname(relative_path)
            
            # Start with the provided prefix if any
            full_prefix = prefix
            
            # If we're handling recursively and the file is in a subdirectory
            if recursive and dir_part:
                if full_prefix:
                    full_prefix = f"{full_prefix.rstrip('/')}/{dir_part}"
                else:
                    full_prefix = dir_part
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add region validation for R2 client**

The R2 client creation looks good, but it always uses 'auto' as the region. Consider adding validation or documentation about the limitations of the region parameter for R2, as it differs from standard S3 behavior.


```diff
 def create_r2_client(
     account_id: str, access_key_id: str, secret_access_key: str
 ) -> boto3.client:
     """
     Create a Cloudflare R2 client using boto3.
 
     Args:
         account_id: Cloudflare account ID.
         access_key_id: R2 access key ID.
         secret_access_key: R2 secret access key.
 
     Returns:
         A boto3 S3 client configured for Cloudflare R2.
+
+    Note:
+        Cloudflare R2 uses 'auto' as the region name regardless of
+        actual deployment region.
     """
     return boto3.client(
         service_name="s3",
         endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
         aws_access_key_id=access_key_id,
         aws_secret_access_key=secret_access_key,
         region_name="auto",  # R2 uses 'auto' as the region
     )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def create_r2_client(
    account_id: str, access_key_id: str, secret_access_key: str
) -> boto3.client:
    """
    Create a Cloudflare R2 client using boto3.

    Args:
        account_id: Cloudflare account ID.
        access_key_id: R2 access key ID.
        secret_access_key: R2 secret access key.

    Returns:
        A boto3 S3 client configured for Cloudflare R2.

    Note:
        Cloudflare R2 uses 'auto' as the region name regardless of
        actual deployment region.
    """
    return boto3.client(
        service_name="s3",
        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
        aws_access_key_id=access_key_id,
        aws_secret_access_key=secret_access_key,
        region_name="auto",  # R2 uses 'auto' as the region
    )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,278 @@
+#!/usr/bin/env python
+
+from typing import Dict, List, Any, Optional
+import argparse
+import os
+import sys
+import boto3
+from botocore.exceptions import ClientError
+import mimetypes
+import logging
+
+
+logging.basicConfig(
+    level=logging.INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def generate_object_key(filepath: str, prefix: Optional[str] = None) -> str:
+    """
+    Generate a unique object key for R2 storage based on the file path.
+
+    Args:
+        filepath: Path to the file to be uploaded.
+        prefix: Optional prefix to add to the object key.
+
+    Returns:
+        A unique object key for R2 storage.
+    """
+    filename = os.path.basename(filepath)
+
+    if prefix:
+        prefix = prefix.rstrip("/") + "/"
+        return f"{prefix}{filename}"
+
+    return filename
+
+
+def get_content_type(filepath: str) -> str:
+    """
+    Determine the content type of a file.
+
+    Args:
+        filepath: Path to the file.
+
+    Returns:
+        The content type of the file.
+    """
+    content_type, _ = mimetypes.guess_type(filepath)
+    return content_type or "application/octet-stream"
+
+
+def create_r2_client(
+    account_id: str, access_key_id: str, secret_access_key: str
+) -> boto3.client:
+    """
+    Create a Cloudflare R2 client using boto3.
+
+    Args:
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+
+    Returns:
+        A boto3 S3 client configured for Cloudflare R2.
+    """
+    return boto3.client(
+        service_name="s3",
+        endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
+        aws_access_key_id=access_key_id,
+        aws_secret_access_key=secret_access_key,
+        region_name="auto",  # R2 uses 'auto' as the region
+    )
+
+
+def upload_file(
+    client: boto3.client, filepath: str, bucket_name: str, object_key: str
+) -> bool:
+    """
+    Upload a file to Cloudflare R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        filepath: Path to the file to be uploaded.
+        bucket_name: Name of the R2 bucket.
+        object_key: Key for the object in R2 storage.
+
+    Returns:
+        True if the upload was successful, False otherwise.
+    """
+    try:
+        content_type = get_content_type(filepath)
+
+        with open(filepath, "rb") as file:
+            client.put_object(
+                Bucket=bucket_name,
+                Key=object_key,
+                Body=file,
+                ContentType=content_type,
+            )
+        return True
+
+    except ClientError as e:
+        logger.error(f"Error uploading {filepath}: {e}")
+        return False
+
+    except Exception as e:
+        logger.error(f"Unexpected error uploading {filepath}: {e}")
+        return False
+
+
+def process_directory(
+    client: boto3.client,
+    directory: str,
+    bucket_name: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+) -> Dict[str, List[Any]]:
+    """
+    Process a directory and upload files to R2.
+
+    Args:
+        client: Boto3 S3 client configured for R2.
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+
+    Returns:
+        A dictionary with lists of successful and failed uploads.
+    """
+    if not os.path.exists(directory):
+        logger.error(f"Directory '{directory}' not found")
+        sys.exit(1)
+
+    if not os.path.isdir(directory):
+        logger.error(f"'{directory}' is not a directory")
+        sys.exit(1)
+
+    results = {"successful": [], "failed": []}
+
+    for root, dirs, files in os.walk(directory):
+        if not recursive and root != directory:
+            continue
+
+        for filename in files:
+            filepath = os.path.join(root, filename)
+
+            relative_path = os.path.relpath(filepath, directory)
+            dir_part = os.path.dirname(relative_path)
+
+            if prefix and dir_part and recursive:
+                full_prefix = f"{prefix.rstrip('/')}/{dir_part}"
+            elif prefix:
+                full_prefix = prefix
+            elif dir_part and recursive:
+                full_prefix = dir_part
+            else:
+                full_prefix = None
+
+            object_key = generate_object_key(filepath, full_prefix)
+
+            if upload_file(client, filepath, bucket_name, object_key):
+                results["successful"].append(
+                    {"file": filepath, "object_key": object_key}
+                )
+                logger.info(f"Uploaded: {filepath} -> {object_key}")
+            else:
+                results["failed"].append(filepath)
+                logger.warning(f"Failed to upload: {filepath}")
+
+    return results
+
+
+def main(
+    directory: str,
+    bucket_name: str,
+    account_id: str,
+    access_key_id: str,
+    secret_access_key: str,
+    prefix: Optional[str] = None,
+    recursive: bool = False,
+    verbose: bool = False,
+) -> None:
+    """
+    Main function to upload files from a directory to Cloudflare R2.
+
+    Args:
+        directory: Directory containing files to upload.
+        bucket_name: Name of the R2 bucket.
+        account_id: Cloudflare account ID.
+        access_key_id: R2 access key ID.
+        secret_access_key: R2 secret access key.
+        prefix: Optional prefix for object keys.
+        recursive: If True, process subdirectories recursively.
+        verbose: If True, enable verbose logging.
+    """
+    if verbose:
+        logger.setLevel(logging.DEBUG)
+
+    client = create_r2_client(account_id, access_key_id, secret_access_key)
+
+    results = process_directory(
+        client, directory, bucket_name, prefix, recursive
+    )
+
+    logger.info(
+        f"Upload complete: {len(results['successful'])} successful, {len(results['failed'])} failed"
+    )
+
+    if results["failed"]:
+        logger.warning("Failed uploads:")
+        for filepath in results["failed"]:
+            logger.warning(f"  - {filepath}")
+
+
+def get_parser() -> argparse.ArgumentParser:
+    """
+    Create and configure the argument parser for the script.
+
+    Returns:
+        An ArgumentParser object configured with the script's arguments.
+    """
+    parser = argparse.ArgumentParser(
+        description="Upload files from a directory to Cloudflare R2 storage."
+    )
+    parser.add_argument(
+        "-d",
+        "--directory",
+        required=True,
+        help="Directory containing files to upload.",
+    )
+    parser.add_argument(
+        "-b",
+        "--bucket-name",
+        default="lingetic-speech-recordings",
+        help="Name of the R2 bucket.",
+    )
+    parser.add_argument(
+        "-a", "--account-id", required=True, help="Cloudflare account ID."
+    )
+    parser.add_argument(
+        "-k", "--access-key-id", required=True, help="R2 access key ID."
+    )
+    parser.add_argument(
+        "-s", "--secret-access-key", required=True, help="R2 secret access key."
+    )
+    parser.add_argument(
+        "-p", "--prefix", help="Optional prefix for object keys in R2."
+    )
+    parser.add_argument(
+        "-r",
+        "--recursive",
+        action="store_true",
+        help="Recursively process subdirectories.",
+    )
+    parser.add_argument(
+        "-v", "--verbose", action="store_true", help="Enable verbose logging."
+    )
+
+    return parser
+
+
+if __name__ == "__main__":
+    parser = get_parser()
+    args = parser.parse_args()
+
+    main(
+        directory=args.directory,
+        bucket_name=args.bucket_name,
+        account_id=args.account_id,
+        access_key_id=args.access_key_id,
+        secret_access_key=args.secret_access_key,
+        prefix=args.prefix,
+        recursive=args.recursive,
+        verbose=args.verbose,
+    )
path: scripts/scripts/upload_to_r2.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Check for sensitive information leaks**

When running with verbose logging, be careful not to log sensitive information like access keys. Consider adding code to mask sensitive information in logs.

```diff
 if __name__ == "__main__":
     parser = get_parser()
     args = parser.parse_args()
 
+    # Mask sensitive information for logging
+    def get_masked_value(value):
+        if not value:
+            return None
+        return value[:4] + "..." + value[-4:] if len(value) > 8 else "****"
+
+    if args.verbose:
+        logger.debug(f"Account ID: {get_masked_value(args.account_id)}")
+        logger.debug(f"Access Key ID: {get_masked_value(args.access_key_id)}")
+        logger.debug(f"Secret Access Key: [MASKED]")
+        logger.debug(f"Directory: {args.directory}")
+        logger.debug(f"Bucket: {args.bucket_name}")
+        logger.debug(f"Prefix: {args.prefix}")
+        logger.debug(f"Recursive: {args.recursive}")
+
     main(
         directory=args.directory,
         bucket_name=args.bucket_name,
         account_id=args.account_id,
         access_key_id=args.access_key_id,
         secret_access_key=args.secret_access_key,
         prefix=args.prefix,
         recursive=args.recursive,
         verbose=args.verbose,
     )
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
if __name__ == "__main__":
    parser = get_parser()
    args = parser.parse_args()

    # Mask sensitive information for logging
    def get_masked_value(value):
        if not value:
            return None
        return value[:4] + "..." + value[-4:] if len(value) > 8 else "****"

    if args.verbose:
        logger.debug(f"Account ID: {get_masked_value(args.account_id)}")
        logger.debug(f"Access Key ID: {get_masked_value(args.access_key_id)}")
        logger.debug(f"Secret Access Key: [MASKED]")
        logger.debug(f"Directory: {args.directory}")
        logger.debug(f"Bucket: {args.bucket_name}")
        logger.debug(f"Prefix: {args.prefix}")
        logger.debug(f"Recursive: {args.recursive}")

    main(
        directory=args.directory,
        bucket_name=args.bucket_name,
        account_id=args.account_id,
        access_key_id=args.access_key_id,
        secret_access_key=args.secret_access_key,
        prefix=args.prefix,
        recursive=args.recursive,
        verbose=args.verbose,
    )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-19T16:36:19Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-04-19T16:33:09Z
body: [vc]: #eSjRw1DzLgft2zj8ysp0v8NNI4gbs5s33iXgau3YMY0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJsaW5nZXRpYyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9hYmhpa3IyMTAwcy1wcm9qZWN0cy9saW5nZXRpYy8yQ2Roc3FtNXZXVVI3NlhLNUxQbnZvanNxWFpIIiwicHJldmlld1VybCI6ImxpbmdldGljLWdpdC0wNC0xOS1hZGRzY3JpcHR0b3VwbG9hZHRvcjItYWJoaWtyMjEwMHMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJsaW5nZXRpYy1naXQtMDQtMTktYWRkc2NyaXB0dG91cGxvYWR0b3IyLWFiaGlrcjIxMDBzLXByb2plY3RzLnZlcmNlbC5hcHAifSwicm9vdERpcmVjdG9yeSI6ImxpbmdldGljLW5leHRqcy1mcm9udGVuZCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **lingetic** | ✅ Ready ([Inspect](https://vercel.com/abhikr2100s-projects/lingetic/2Cdhsqm5vWUR76XK5LPnvojsqXZH)) | [Visit Preview](https://lingetic-git-04-19-addscripttouploadtor2-abhikr2100s-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/lingetic-git-04-19-addscripttouploadtor2-abhikr2100s-projects.vercel.app?via=pr-comment-feedback-link) | Apr 19, 2025 4:34pm |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-19T16:33:12Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new script, `upload_to_r2.py`, was added to provide command-line functionality for uploading files from a local directory to Cloudflare R2 storage using the boto3 S3 client. The script includes argument parsing, error handling, and logging, supporting both recursive and non-recursive uploads with optional object key prefixes. To support this, the `boto3` dependency was added to the project's requirements. The `upload_to_r2` module was also exposed via the package's `__init__.py` to make it accessible as part of the public API.

## Changes

| File(s)                                   | Change Summary                                                                                              |
|--------------------------------------------|------------------------------------------------------------------------------------------------------------|
| scripts/pyproject.toml                     | Added `boto3` (>=1.37.37) to the list of dependencies.                                                     |
| scripts/scripts/__init__.py                | Imported and added `upload_to_r2` to `__all__`, exposing it as part of the public API.                     |
| scripts/scripts/upload_to_r2.py            | Introduced a new CLI tool for uploading files/directories to Cloudflare R2 using boto3, with full logic.   |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant CLI Parser
    participant Main Function
    participant R2 Client
    participant Cloudflare R2

    User->>CLI Parser: Provide arguments (dir, bucket, creds, etc.)
    CLI Parser->>Main Function: Parse and pass arguments
    Main Function->>R2 Client: Create boto3 client for R2
    Main Function->>Main Function: Process directory, generate object keys
    loop For each file
        Main Function->>R2 Client: Upload file to bucket
        R2 Client->>Cloudflare R2: PUT Object
        Cloudflare R2-->>R2 Client: Response (success/fail)
        R2 Client-->>Main Function: Upload result
    end
    Main Function->>User: Print summary of uploads
```

## Poem

> 🐇  
> A hop, a skip, a cloudward leap,  
> To R2’s vaults, our files now sweep!  
> With boto3 and Python’s might,  
> We upload by day, we upload by night.  
> Prefix or not, recursive too—  
> The bunny’s code delivers true!  
> Hooray for storage, swift and bright!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS4BBWvUQMK8brkgFI2bhfxp6HgCUAJgMAOWxmAUouIIAGEIBVRGj0AQQwAGsKMCCARljYo31jcCgyenwAMxwCYjJlGnomVnYuXn5hUXEpGXkmJSpVdS0dYpMoOFRUTBrCUnIqRoUWjE5IKgB3SEQI5jQKeTkFAZU1TW1dMEMS0wNHZ1dEAHpuWV58ETENAmYLDgMAERAgwAYhBkBsAEk6gtqHRtrt9vIqoxYJhSIgzLASJAlNxymQGPILEg3PAsLhsZAAAb3Fy4Z6vd6fTQ/CzUyCVeBWSAbNDILy0OH0I5+WjkojoSDkLbcNAMDJoUjrEgAR2w8AoJDYqy41IE+AIAGYORt1LApcxyfBmBFIFIKIh4PgsCjchojQB2D3eyChfD8SmUVHo6S8yg4vZKdwBoO4kj4jBKDAMeBh/B8TDIoN8OUUNwouNcqwaIxGMEQiw0RbOjDIDxxpQMCz7ai15AokgAD24GaWGZ42AEJIYkHY6jTmLCLpIpaBAKKtzpjyey4ZTwA+hvrbgtxpXv956DwVCYQ14TtWEj+NUGGiMBisZHFNgedSvD4/BuCBuKEFTfy6D2PCDZUjavb5tsuBwjqDLoEmKAphY2BKPQ5LuFS1JbmgFgWFuHIkogZIUpha7PGRm7bhg6h7q8HLFrOMAIMgd6hmOPb4Mk9aYR+vi0N++C/v+kDMC+PKAXmBbVHGcoKkqJAAOTINwQ4jhCAAKkIADToLh+Bmg+KBuB4UQoMwEFLOKWpiBY8gbNiJE4rJirKlM5l9nQGh+gGhrYnwlTYCm4gujh/B8ERFDYGI2BUBYIYPmGGwRiJfizmWx6VtWbYuvWsZUk2LY1jlN7sRZ8IDipw7wKO47iNIRSQAAsmJOJinQeq8V+P5/hy4EefQmADcB/gBlhG44XhG69VgtJOPS5FzSuW47rRsjUoCwIGBAYBGBRFGdfx3VBPusiHsCFanvM54OIiBwlaxCVTjY0okFs6myJSLrbItZLIOSuAUC+DAgQGB2ctyYaVIDzBSj4DChVZXQZvIHgAMI+ChlSFTiwRQRm8meE6hlxgaxqQAAykajAkuwCgYFyRAxfClQDsEXmQm47wSPASjIAFQW1jh6gowGV2LDiggspAGQkLIHauILuHyLwJBcl2dA6Uo1ZWuQTWQo1ACidM0Ks7hvJDA4MYgOmDYwWpwlKpP4FTzZpqb0EUKQ4jE1S6P4Jj2OQLj5S9v9vLmpARCSGQdt0OOOGIF5cA4muCLcBZAreHxEqIeK3O0NgoVW+HlJjhQgN8PetAkoZLN8K7tO24F3b4mI8LdsDCs5TpPhEFHhmUBXyDykwFDig+tmlgYHM8IDwOIFxUqI2IyOQEc+BdxgE3yNZMVOlItnuFQDpE5KOwCMvBDONIOlMHWANRd7kpoOngO8PADuS100uyzI/LlVgBiPBqCwGHghDewUt5xRVmrG+8F6AHRziQeUFogH2QhgoXCXQc5ah2FWDs1QdgMHnogAKcVbaVG0FYBBWc/CJyYpGbQgDApiFrGFO80gAZwhYjTU2ThkGQM1pqJGd13gkIlDbBCvdh4IivHdFEYN/a4GaHAs0pcZgQLYXXe0lADTJEgL3fuRAp42CwPsRmsFgGOmDKgJQXJyAjUgFXHkzQ9hJjADXHE5IVJwW0ZfZGOkBBRRlm4LebAdJ+wDvsVqxD/am0hAAERtsQ6QyAZZyx0pokK0CtRqx0rvR0WiWxEEkfQB0ejhZJypKndgoj8BhzzIvcxER2BgLQhgCQ+AZbcUYehMGvdqroFiWPCUk8jAAHkHQTR0nGGpKYX54O4VKZo3gSA0EgKjAAMpCGM+A4raMQYZfxd074mzgmjDGtAsbRKDkEEuFo74MxiioHkWS6z5MENgIiZcK5OMGjXEp8D4zQQhvQQxEop7lhPFWBo7YYwYRxAVVskCCGlX6mFQcVUaqrAnPVLakAABiLDIFAVQnqMWcINxfzEBudJAAKBicpKRcAijpGB8AuxcHGZvHCABtCKABdSAABeby5AACUug9BQQoNNGke0frPAOgJISJ11pQCJQLL6bVaDkrWRuU57BvzmwZRDJlsAWUA0lVcGVcrZoPHXPtWhh1BJ/jVfizVrDtXDT1Pwylf4DW8NwHSkecTdw80tRQZJJDaWy23LqmVOlkj8N3CPVJsbToyutdKp2RoNAN1WHahVDqlXOpVW6uiHriVsJ1R1MtDE6UFrWLm/NQadKMpAZGwJwT9VhOsImjoLIM2RuzWvfAeyi2KtXFO5VR13Uaurd6slNIxHpuObIRtQauAtqbUI6yV9M2srXj23cfau2z1VhyrlPKLD8oBkK0V/pyD5NEHvaO27x1xVFQShOJBR0JOqrgO9UbICbNJLy0xsgBUCsnSW6dcHZ2uuOpWhdWqzE+ppHsckdL13nqCQqXtaA2DntDYFcNCaj1poXhm+N57k1alTSk6j6Tz3ss5ZAblkC+WCpFWKkgL6GBvqkB+vZvGf0WGSDpcpnF+0GlE9+39o6n0kFg/NeDanEOqpQ4Sxd6Hl3Ui9huRplA6WjvMcZjQNhPYtNWOpfYyRZWIXlVOp1n4XVabWhtBcW0rg3DKOA6oaA8BzHqOLJoLBYJcE2LIvYd0jj9GUEMc4oxfPjGWFacjiBfwkG5q9OgG4iL7DcMUAwaWAAcsQAAslQABsuRPRBEqJUBgnojS0AYLkSrABWXIZXKtoC6wATjKzVhgg2jRGgEEEEgRoutlZIEEIUlxrhpdcTRHmWWtS5aSvxcoy2gA= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=abhi-kr-2100/Lingetic&utm_content=202):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: abhi-kr-2100
created_at: 2025-04-19T16:33:20Z
body: * **#204** <a href="https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/204?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#203** <a href="https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/203?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#202** <a href="https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/202?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/202?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: OWNER
user: login: abhi-kr-2100
created_at: 2025-04-19T20:07:35Z
body: ### Merge activity

* **Apr 19, 4:07 PM EDT**: A user started a stack merge that includes this pull request via [Graphite](https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/202).
* **Apr 19, 4:07 PM EDT**: A user merged this pull request with [Graphite](https://app.graphite.dev/github/pr/abhi-kr-2100/Lingetic/202).

author_association: OWNER