From 6f4acbdcc6ccd437aecac0bf61175d284f503448 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Wed, 10 Dec 2025 13:31:13 +0000 Subject: [PATCH 01/19] updated postman readme --- .gitignore | 3 ++ README.md | 6 ++-- postman/README.md | 91 +++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 3a6fee4..42660b6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ __pycache__/ .idea/ *.swp *.swo + +# Serena AI Assistant +.serena/ diff --git a/README.md b/README.md index 8675a33..5504f70 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,10 @@ The Nitro Platform API provides programmatic access to document processing capab #### Using Postman 1. Import the collection from `postman/Platform-API.postman_collection.json` -2. Configure environment variables (see `postman/README.md`) -3. Get a bearer token and run sample requests +2. Configure collection variables (see `postman/README.md`) +3. Get a bearer token +4. Select test files for each request +5. Run the sample requests #### Using Power Automate 1. Create a custom connector using instructions in `power-automate/` diff --git a/postman/README.md b/postman/README.md index 41c6806..7048eac 100644 --- a/postman/README.md +++ b/postman/README.md @@ -11,15 +11,18 @@ This folder contains the Postman collection for the Nitro Platform API with exam 3. Select `Platform-API.postman_collection.json` 4. The collection will appear in your workspace -### 2. Configure Environment Variables - -Create a new environment in Postman with these variables: - -| Postman Variable | Python .env Variable | Description | Example Value | -|------------------|---------------------|-------------|---------------| -| `baseUrl` | `PLATFORM_BASE_URL` | API base URL | `https://api.gonitro.dev` | -| `clientID` | `PLATFORM_CLIENT_ID` | Your client ID | `your-client-id` | -| `clientSecret` | `PLATFORM_CLIENT_SECRET` | Your client secret | `your-client-secret` | +### 2. Configure Collection Variables + +1. Click on the collection name in the sidebar +2. Go to the **Variables** tab +3. Set the **CURRENT VALUE** for these variables: + +| Variable | Description | Example Value | +|----------|-------------|---------------| +| `baseUrl` | API base URL | `https://api.gonitro.dev` | +| `clientID` | Your client ID | `your-client-id` | +| `clientSecret` | Your client secret | `your-client-secret` | +| `repoPath` | Absolute path to this repo | `/Users/you/github/nitro-platform-integrations` | | `token` | (auto-populated) | Bearer token | `eyJ0eXAiOiJKV1Q...` | **Note**: If you're also using the Python samples, you can use the same credential values from your `.env` file - just use the Postman variable names shown above. @@ -41,7 +44,16 @@ The collection is organized into folders: - **Transformations**: Modify PDFs (compress, merge, split, etc.) - **Jobs**: Check status and get results for async operations -## Example Workflow +To run them: +1. Select any request (e.g., **Word → PDF**) +2. Go to **Body** tab → **form-data** +3. **Manually select the file** using the file picker: + - Click on the file field + - Navigate to your `repoPath` folder + - Select the appropriate file from the [File Reference Guide](#file-reference-guide) below +4. Click **Send** + +### Example Workflow 1. **Get Token**: Run "Get Bearer Token" request 2. **Convert Document**: Use "Word → PDF" with a sample .docx file @@ -78,3 +90,62 @@ To get your API credentials: 2. Navigate to **Settings** → **API** 3. Click **Create Application** 4. Name your application and save the Client ID and Client Secret + +## Test Files Reference + +When testing endpoints in Postman, use these test files for each operation: + +### Conversions + +| Endpoint | Test File | Location | +|----------|-----------|----------| +| Word → PDF | Analysis.docx | `test_files/test-batch/Analysis.docx` | +| Excel → PDF | Feedback.xlsx | `test_files/test-batch/Feedback.xlsx` | +| PowerPoint → PDF | SamplePPTX.pptx | `test_files/SamplePPTX.pptx` | +| Image → PDF | GoNitro.png | `test_files/GoNitro.png` | +| PDF → Word | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| PDF → Excel | Sample Tables.pdf | `test_files/test-pdfs/Sample Tables.pdf` | +| PDF → Image | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | + +### Extractions + +| Endpoint | Test File | Location | +|----------|-----------|----------| +| Extract PDF Text | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Extract PDF Form Data | BOB - Student-Loan-Application-Form.pdf | `test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf` | +| Extract PDF Table Data | Sample Tables.pdf | `test_files/test-pdfs/Sample Tables.pdf` | +| Extract and Autodetect Bounding Boxes for PII | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Extract Bounding Boxes for Strings | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Get PDF Properties | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Set PDF Properties | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | + +### Transformations + +| Endpoint | Test File | Location | +|----------|-----------|----------| +| Redact Bounding boxes (scrub PII) | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Compress | BOB - Student-Loan-Application-Form.pdf | `test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf` | +| Flatten | Sample Tables.pdf | `test_files/test-pdfs/Sample Tables.pdf` | +| Rotate Pages | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Delete Pages | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Split | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Merge | Sample Tables.pdf + SampleResume.pdf | `test_files/test-pdfs/` (multiple files) | +| Password Protect | SampleResume.pdf | `test_files/test-pdfs/SampleResume.pdf` | +| Password Remove | PDF-withpassword.pdf | `test_files/PDF-withpassword.pdf` | + +### Available Test Files + +All test files are located in the `test_files/` directory: +``` +test_files/ +├── test-pdfs/ +│ ├── SampleResume.pdf # Resume with text and PII +│ ├── Sample Tables.pdf # PDF with table data +│ └── BOB - Student-Loan-Application-Form.pdf # PDF with form fields +├── test-batch/ +│ ├── Analysis.docx # Word document +│ └── Feedback.xlsx # Excel spreadsheet +├── SamplePPTX.pptx # PowerPoint presentation +├── GoNitro.png # Sample image +└── PDF-withpassword.pdf # Password-protected PDF +``` \ No newline at end of file From 90fefd819ec50d4a3b1461c5840973e42f7b0c15 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Thu, 11 Dec 2025 12:23:19 +0000 Subject: [PATCH 02/19] first iteration on the prepare_pdf_for_distribution scrpt (removing properties still missing) --- samples/python/.gitignore | 8 ++ .../python/prepare_pdf_for_distribution.py | 124 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100755 samples/python/prepare_pdf_for_distribution.py diff --git a/samples/python/.gitignore b/samples/python/.gitignore index 4ad6dc2..2c06782 100644 --- a/samples/python/.gitignore +++ b/samples/python/.gitignore @@ -11,3 +11,11 @@ __pycache__/ *.pyo *.pyd .Python + +# Virtual environments (never commit these!) +venv/ +env/ +.venv/ +.env/ +platform-integrations/ +nitro-venv/ diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py new file mode 100755 index 0000000..a6bc4ed --- /dev/null +++ b/samples/python/prepare_pdf_for_distribution.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +🔒 PREPARE PDF FOR DISTRIBUTION +================================ + +THE STORY: +Your company handles sensitive documents - contracts, proposals, financial reports. +These documents contain internal metadata that could reveal private information: + • Author names and email addresses + • Company internal file paths + • Edit history and revision dates + +Before sharing ANY document externally, we MUST remove this metadata to protect +employee privacy, company confidentiality, and ensure legal compliance. + +This script automates the secure preparation workflow! + +WORKFLOW: +1. Place Word/Excel/PowerPoint documents in input folder +2. Script converts to PDF → compresses → strips metadata +3. Find distribution-ready PDFs in output folder + +USAGE: + python prepare_pdf_for_distribution.py + +EXAMPLE: + python prepare_pdf_for_distribution.py ./confidential ./ready_for_clients +""" + +import sys +from pathlib import Path +from platform_api import PlatformAPIClient + + +def main(): + # Check command-line arguments + if len(sys.argv) != 3: + print("Usage: python prepare_pdf_for_distribution.py ") + sys.exit(1) + + # Get folder paths from arguments + input_folder = Path(sys.argv[1]) + output_folder = Path(sys.argv[2]) + + # Validate input folder exists + if not input_folder.exists() or not input_folder.is_dir(): + print(f"❌ Error: Invalid input folder: {input_folder}") + sys.exit(1) + + # Create output folder if needed + output_folder.mkdir(parents=True, exist_ok=True) + + # Find all Office documents (Word, Excel, PowerPoint) + patterns = ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt'] + files = [] + for pattern in patterns: + files.extend(input_folder.glob(pattern)) + + if not files: + print(f"❌ No Office documents found in {input_folder}") + sys.exit(1) + + print(f"📋 Found {len(files)} document(s) to process\n") + + # Initialize API client (loads credentials from .env) + client = PlatformAPIClient() + + # Process each document + success_count = 0 + failed_count = 0 + + for i, doc in enumerate(files, 1): + print(f"[{i}/{len(files)}] Processing: {doc.name}") + + try: + # Step 1: Convert to PDF + print(" 🔐 Converting to PDF...") + pdf_bytes = client.convert(doc, "pdf") + temp_pdf = output_folder / f"{doc.stem}_temp.pdf" + temp_pdf.write_bytes(pdf_bytes) + + # Step 2: Compress PDF + print(" 📦 Compressing...") + compressed = client.compress(temp_pdf, level=2) + temp_pdf.write_bytes(compressed) + + # Step 3: Remove all metadata/properties (PRIVACY PROTECTION) + # TODO: The set-properties API endpoint needs proper parameters + # Current issue: 422 error with empty params + # Skipping for now until API documentation is clarified + print(" 🔄 Metadata removal (needs API param clarification)...") + clean_pdf = compressed # Use compressed version for now + + # Future: Remove annotations (API in development) + # clean_pdf = client.remove_annotations(temp_pdf) + + # Future: Make accessible (API in development) + # clean_pdf = client.make_accessible(temp_pdf) + + # Save final PDF + final_pdf = output_folder / f"{doc.stem}.pdf" + final_pdf.write_bytes(clean_pdf) + temp_pdf.unlink() # Delete temp file + + print(f" ✅ Secured: {final_pdf.name}\n") + success_count += 1 + + except Exception as e: + print(f" ❌ FAILED: {e}\n") + failed_count += 1 + if temp_pdf.exists(): + temp_pdf.unlink() + + # Display summary + print("=" * 60) + print(f"✅ {success_count} document(s) secured") + if failed_count > 0: + print(f"⚠️ {failed_count} document(s) FAILED - do NOT distribute!") + print(f"📂 Output: {output_folder.absolute()}") + print("=" * 60) + + +if __name__ == "__main__": + main() From 06cb9049987b1ee4edbfe4d4d36ac4bcaf803c0c Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Thu, 11 Dec 2025 15:35:12 +0000 Subject: [PATCH 03/19] added remotion of properties from pdf --- .../python/prepare_pdf_for_distribution.py | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index a6bc4ed..896f10b 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -3,28 +3,31 @@ 🔒 PREPARE PDF FOR DISTRIBUTION ================================ -THE STORY: -Your company handles sensitive documents - contracts, proposals, financial reports. -These documents contain internal metadata that could reveal private information: - • Author names and email addresses - • Company internal file paths - • Edit history and revision dates +The script exemplifies a typical workflow of marketing brochure distribution. +As a marketing professional, it's necessary to share company brochures externally +while ensuring they comply with corporate distribution standards. Word document +properties can expose internal information such as author names, template paths, +revision history, and company file structures that should remain confidential. -Before sharing ANY document externally, we MUST remove this metadata to protect -employee privacy, company confidentiality, and ensure legal compliance. +This workflow automates compliant document preparation. The script processes each +file individually - for every brochure in the input folder, it converts the Word +document into PDF format, then compresses the file to reduce size and optimize +transmission, and finally removes all metadata properties to ensure privacy and +confidentiality. Each processed file is saved to the output folder, resulting in +distribution-ready brochures. -This script automates the secure preparation workflow! - -WORKFLOW: -1. Place Word/Excel/PowerPoint documents in input folder -2. Script converts to PDF → compresses → strips metadata -3. Find distribution-ready PDFs in output folder +COMPANY DISTRIBUTION STANDARDS: + ✓ PDF format (prevents editing) + ✓ Compressed (optimized file size) + ✓ Properties removed (no metadata exposure) + ⏳ Annotations removed (feature in development) + ⏳ Accessibility enabled (feature in development) USAGE: python prepare_pdf_for_distribution.py EXAMPLE: - python prepare_pdf_for_distribution.py ./confidential ./ready_for_clients + python prepare_pdf_for_distribution.py ../../test_files/test-batch ./output """ import sys @@ -85,11 +88,33 @@ def main(): temp_pdf.write_bytes(compressed) # Step 3: Remove all metadata/properties (PRIVACY PROTECTION) - # TODO: The set-properties API endpoint needs proper parameters - # Current issue: 422 error with empty params - # Skipping for now until API documentation is clarified - print(" 🔄 Metadata removal (needs API param clarification)...") - clean_pdf = compressed # Use compressed version for now + print(" 🔒 Removing metadata properties...") + + # First, get current PDF properties + properties_response = client._request("extractions", "get-properties", temp_pdf, {}) + current_properties = properties_response.get("result", {}) + + # Display current properties + print(f" Current properties found:") + for key, value in current_properties.items(): + if key != "file" and value: # Skip empty values and file object + print(f" • {key}: {value}") + + # Build params to clear properties + # Note: Only user-editable properties can be cleared via the API + # System properties (creator, producer, dates, trapped) are read-only in PDF spec + writable_props = ["title", "author", "subject", "keywords"] + clear_properties = {} + for key in writable_props: + if key in current_properties: + clear_properties[key] = "" # Set to empty string to clear + + # Display which properties will be removed + print(f" Removing properties: {', '.join(clear_properties.keys())}") + + # Now set properties to empty strings + clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, clear_properties) + temp_pdf.write_bytes(clean_pdf) # Future: Remove annotations (API in development) # clean_pdf = client.remove_annotations(temp_pdf) From d9652401694928255a9b52e4c1c35b8fd48fdc46 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Fri, 12 Dec 2025 01:07:54 +0000 Subject: [PATCH 04/19] fix deleting of properties --- .../python/prepare_pdf_for_distribution.py | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index 896f10b..ae4cb1b 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -35,6 +35,25 @@ from platform_api import PlatformAPIClient +def get_pdf_properties_to_clear(client, pdf_path): + """ + Get all PDF metadata properties that exist (to be cleared). + + Args: + client: PlatformAPIClient instance + pdf_path: Path to the PDF file + + Returns: + dict: Dictionary of properties to clear (all set to empty string) + """ + # Get current PDF properties + properties_response = client._request("extractions", "get-properties", pdf_path, {}) + current_properties = properties_response.get("result", {}) + + # Return all properties (except 'file') set to empty string + return {k: "" for k in current_properties.keys() if k != "file"} + + def main(): # Check command-line arguments if len(sys.argv) != 3: @@ -90,30 +109,39 @@ def main(): # Step 3: Remove all metadata/properties (PRIVACY PROTECTION) print(" 🔒 Removing metadata properties...") - # First, get current PDF properties - properties_response = client._request("extractions", "get-properties", temp_pdf, {}) - current_properties = properties_response.get("result", {}) - - # Display current properties - print(f" Current properties found:") - for key, value in current_properties.items(): - if key != "file" and value: # Skip empty values and file object - print(f" • {key}: {value}") - - # Build params to clear properties - # Note: Only user-editable properties can be cleared via the API - # System properties (creator, producer, dates, trapped) are read-only in PDF spec - writable_props = ["title", "author", "subject", "keywords"] - clear_properties = {} - for key in writable_props: - if key in current_properties: - clear_properties[key] = "" # Set to empty string to clear + # Get properties to clear from helper function + properties_to_clear = get_pdf_properties_to_clear(client, temp_pdf) - # Display which properties will be removed - print(f" Removing properties: {', '.join(clear_properties.keys())}") + if properties_to_clear: + # Try to clear all properties + try: + clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, properties_to_clear) + print(f" ✓ Cleared: {', '.join(properties_to_clear.keys())}") + except Exception as e: + if "422" in str(e): + # Some properties are read-only, try each individually + cleared = [] + read_only = [] + + for prop_key in properties_to_clear.keys(): + try: + client._request_bytes("transformations", "set-properties", temp_pdf, {prop_key: ""}) + cleared.append(prop_key) + except: + read_only.append(prop_key) + + clean_pdf = temp_pdf.read_bytes() + + if cleared: + print(f" ✓ Cleared: {', '.join(cleared)}") + if read_only: + print(f" ⚠ Read-only (not cleared): {', '.join(read_only)}") + else: + raise + else: + print(" ℹ No properties to clear") + clean_pdf = temp_pdf.read_bytes() - # Now set properties to empty strings - clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, clear_properties) temp_pdf.write_bytes(clean_pdf) # Future: Remove annotations (API in development) From b8b66e4c2327252907a02ce0bb0ced332f39a32e Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Mon, 15 Dec 2025 19:47:48 +0000 Subject: [PATCH 05/19] updated pdf_for_distribution script and added new test documents --- .gitignore | 2 + samples/python/{ => api}/platform_api.py | 0 .../helper_functions/document_helpers.py | 43 ++++++++ .../python/prepare_pdf_for_distribution.py | 98 +++--------------- .../Marketing_Brochure_Product_A_Rich.docx | Bin 0 -> 39670 bytes .../Marketing_Brochure_Product_B_Rich.docx | Bin 0 -> 39775 bytes .../Marketing_Brochure_Product_C_Rich.docx | Bin 0 -> 39816 bytes 7 files changed, 62 insertions(+), 81 deletions(-) rename samples/python/{ => api}/platform_api.py (100%) create mode 100644 samples/python/helper_functions/document_helpers.py create mode 100644 test_files/pdf-distribution/Marketing_Brochure_Product_A_Rich.docx create mode 100644 test_files/pdf-distribution/Marketing_Brochure_Product_B_Rich.docx create mode 100644 test_files/pdf-distribution/Marketing_Brochure_Product_C_Rich.docx diff --git a/.gitignore b/.gitignore index 42660b6..d1f3c83 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ __pycache__/ # Serena AI Assistant .serena/ + +.plataform-integrations/ \ No newline at end of file diff --git a/samples/python/platform_api.py b/samples/python/api/platform_api.py similarity index 100% rename from samples/python/platform_api.py rename to samples/python/api/platform_api.py diff --git a/samples/python/helper_functions/document_helpers.py b/samples/python/helper_functions/document_helpers.py new file mode 100644 index 0000000..18c5141 --- /dev/null +++ b/samples/python/helper_functions/document_helpers.py @@ -0,0 +1,43 @@ +""" +Common helper utilities for document processing scripts. +""" + +import sys +from pathlib import Path + + +def validate_and_setup(input_folder, output_folder, file_patterns=None): + """ + Validate input folder and setup output folder. Returns list of files to process. + + Args: + input_folder: Path to the input directory containing files to process + output_folder: Path to the output directory for processed files + file_patterns: List of glob patterns to match files (e.g., ['*.docx', '*.pdf']) + If None, defaults to common Office document formats + + Returns: + List of Path objects for files matching the patterns + """ + # Default patterns for Office documents if none provided + if file_patterns is None: + file_patterns = ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt'] + + # Validate input folder exists + if not input_folder.exists() or not input_folder.is_dir(): + print(f"❌ Error: Invalid input folder: {input_folder}") + sys.exit(1) + + # Create output folder if needed + output_folder.mkdir(parents=True, exist_ok=True) + + # Find all matching files + files = [] + for pattern in file_patterns: + files.extend(input_folder.glob(pattern)) + + if not files: + print(f"❌ No files matching patterns {file_patterns} found in {input_folder}") + sys.exit(1) + + return files diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index ae4cb1b..bc538a7 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -32,26 +32,12 @@ import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient +from helper_functions.document_helpers import validate_and_setup -def get_pdf_properties_to_clear(client, pdf_path): - """ - Get all PDF metadata properties that exist (to be cleared). - - Args: - client: PlatformAPIClient instance - pdf_path: Path to the PDF file - - Returns: - dict: Dictionary of properties to clear (all set to empty string) - """ - # Get current PDF properties - properties_response = client._request("extractions", "get-properties", pdf_path, {}) - current_properties = properties_response.get("result", {}) - - # Return all properties (except 'file') set to empty string - return {k: "" for k in current_properties.keys() if k != "file"} +# Configuration: Properties to remove from PDFs +PROPERTIES_TO_REMOVE = ["title", "author", "subject", "keywords", "creator", "producer"] def main(): @@ -64,24 +50,8 @@ def main(): input_folder = Path(sys.argv[1]) output_folder = Path(sys.argv[2]) - # Validate input folder exists - if not input_folder.exists() or not input_folder.is_dir(): - print(f"❌ Error: Invalid input folder: {input_folder}") - sys.exit(1) - - # Create output folder if needed - output_folder.mkdir(parents=True, exist_ok=True) - - # Find all Office documents (Word, Excel, PowerPoint) - patterns = ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt'] - files = [] - for pattern in patterns: - files.extend(input_folder.glob(pattern)) - - if not files: - print(f"❌ No Office documents found in {input_folder}") - sys.exit(1) - + # Validate and setup + files = validate_and_setup(input_folder, output_folder) print(f"📋 Found {len(files)} document(s) to process\n") # Initialize API client (loads credentials from .env) @@ -94,66 +64,32 @@ def main(): for i, doc in enumerate(files, 1): print(f"[{i}/{len(files)}] Processing: {doc.name}") + temp_pdf = None try: # Step 1: Convert to PDF print(" 🔐 Converting to PDF...") pdf_bytes = client.convert(doc, "pdf") + temp_pdf = output_folder / f"{doc.stem}_temp.pdf" temp_pdf.write_bytes(pdf_bytes) + # Step 2: Compress PDF print(" 📦 Compressing...") - compressed = client.compress(temp_pdf, level=2) - temp_pdf.write_bytes(compressed) - - # Step 3: Remove all metadata/properties (PRIVACY PROTECTION) - print(" 🔒 Removing metadata properties...") - - # Get properties to clear from helper function - properties_to_clear = get_pdf_properties_to_clear(client, temp_pdf) - - if properties_to_clear: - # Try to clear all properties - try: - clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, properties_to_clear) - print(f" ✓ Cleared: {', '.join(properties_to_clear.keys())}") - except Exception as e: - if "422" in str(e): - # Some properties are read-only, try each individually - cleared = [] - read_only = [] - - for prop_key in properties_to_clear.keys(): - try: - client._request_bytes("transformations", "set-properties", temp_pdf, {prop_key: ""}) - cleared.append(prop_key) - except: - read_only.append(prop_key) - - clean_pdf = temp_pdf.read_bytes() - - if cleared: - print(f" ✓ Cleared: {', '.join(cleared)}") - if read_only: - print(f" ⚠ Read-only (not cleared): {', '.join(read_only)}") - else: - raise - else: - print(" ℹ No properties to clear") - clean_pdf = temp_pdf.read_bytes() + compressed_pdf = client.compress(temp_pdf, level=2) - temp_pdf.write_bytes(clean_pdf) + temp_pdf.write_bytes(compressed_pdf) - # Future: Remove annotations (API in development) - # clean_pdf = client.remove_annotations(temp_pdf) + # Step 3: Remove metadata properties + print(" 🔒 Removing metadata...") + properties_to_clear = {prop: "" for prop in PROPERTIES_TO_REMOVE} + clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, properties_to_clear) - # Future: Make accessible (API in development) - # clean_pdf = client.make_accessible(temp_pdf) # Save final PDF final_pdf = output_folder / f"{doc.stem}.pdf" final_pdf.write_bytes(clean_pdf) - temp_pdf.unlink() # Delete temp file + temp_pdf.unlink() print(f" ✅ Secured: {final_pdf.name}\n") success_count += 1 @@ -161,7 +97,7 @@ def main(): except Exception as e: print(f" ❌ FAILED: {e}\n") failed_count += 1 - if temp_pdf.exists(): + if temp_pdf and temp_pdf.exists(): temp_pdf.unlink() # Display summary diff --git a/test_files/pdf-distribution/Marketing_Brochure_Product_A_Rich.docx b/test_files/pdf-distribution/Marketing_Brochure_Product_A_Rich.docx new file mode 100644 index 0000000000000000000000000000000000000000..58cdaf67e91365441244def9ad0c7c16a33cd11e GIT binary patch literal 39670 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 BLLUGC literal 0 HcmV?d00001 diff --git a/test_files/pdf-distribution/Marketing_Brochure_Product_B_Rich.docx b/test_files/pdf-distribution/Marketing_Brochure_Product_B_Rich.docx new file mode 100644 index 0000000000000000000000000000000000000000..5be11a465248f2266bc8ddd3af76aa2bc0d8fb57 GIT binary patch literal 39775 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 Date: Mon, 15 Dec 2025 20:08:37 +0000 Subject: [PATCH 06/19] updated pii-redaction script by: adding story telling and allowing batch processing --- samples/python/smart_redact_pii.py | 131 ++++++++++++++++++++++++----- 1 file changed, 109 insertions(+), 22 deletions(-) diff --git a/samples/python/smart_redact_pii.py b/samples/python/smart_redact_pii.py index 75b45da..052bc18 100644 --- a/samples/python/smart_redact_pii.py +++ b/samples/python/smart_redact_pii.py @@ -1,34 +1,121 @@ -#!/usr/bin/env python -"""Smart redact PII from documents.""" +#!/usr/bin/env python3 +""" +🔒 SMART PII REDACTION +====================== + +The script exemplifies a typical workflow for protecting sensitive customer information. +As a compliance officer or legal professional, it's essential to review and redact +personally identifiable information (PII) from documents before sharing them with +third parties, storing them in public systems, or using them for analysis. Manual +redaction is time-consuming and error-prone, potentially missing sensitive data like +social security numbers, phone numbers, addresses, or email addresses. + +This workflow automates compliant document redaction. The script processes each PDF +file individually - for every document in the input folder, it uses AI-powered PII +detection to identify all instances of sensitive information across all pages, then +automatically applies redactions to permanently remove this data. Each processed file +is saved to the output folder with all PII securely redacted, ready for safe sharing +or archival. + +PRIVACY COMPLIANCE STANDARDS: + ✓ AI-powered PII detection (SSN, phone, email, address) + ✓ Automatic redaction (permanent removal) + ✓ Batch processing (entire folders) + +USAGE: + python smart_redact_pii.py + +EXAMPLE: + python smart_redact_pii.py ../../test_files/test-pdfs ./output +""" import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient +from helper_functions.document_helpers import validate_and_setup -if __name__ == "__main__": + +def main(): + # Check command-line arguments if len(sys.argv) != 3: - print("Usage: python smart_redact_pii.py ") + print("Usage: python smart_redact_pii.py ") sys.exit(1) - input_path = Path(sys.argv[1]) - output_path = Path(sys.argv[2]) + # Get folder paths from arguments + input_folder = Path(sys.argv[1]) + output_folder = Path(sys.argv[2]) - client = PlatformAPIClient() - - print(f"🔍 Detecting PII in {input_path.name}...") - pii_data = client.detect_pii(input_path) + # Validate and setup (only process PDF files) + files = validate_and_setup(input_folder, output_folder, file_patterns=['*.pdf']) + print(f"📋 Found {len(files)} PDF document(s) to process") - pii_boxes = pii_data.get('result', {}).get('PIIBoxes', []) - if not pii_boxes: - print("✅ No PII detected") - sys.exit(0) + # Initialize API client + client = PlatformAPIClient() - print(f"🎯 Found {len(pii_boxes)} PII instances") - print(f"🔒 Redacting PII...") + # Process each document + success_count = 0 + failed_count = 0 + total_pii_count = 0 - redactions = [{"pageIndex": box["pageIndex"], "boundingBox": box["boundingBox"]} - for box in pii_boxes] + for i, pdf_file in enumerate(files, 1): + print(f"[{i}/{len(files)}] Processing: {pdf_file.name}") + + try: + # Step 1: Detect PII in the document + print(" 🔍 Detecting PII...") + pii_data = client.detect_pii(pdf_file) + + # Extract PII bounding boxes from response + pii_boxes = pii_data.get('result', {}).get('PIIBoxes', []) + + if not pii_boxes: + print(" ℹ️ No PII detected - copying original file") + + # Copy original file to output if no PII found + output_file = output_folder / pdf_file.name + output_file.write_bytes(pdf_file.read_bytes()) + + print(f" ✅ Saved: {output_file.name}") + + success_count += 1 + continue + + print(f" 🎯 Found {len(pii_boxes)} PII instance(s)") + total_pii_count += len(pii_boxes) + + # Step 2: Prepare redaction coordinates + print(" 🔒 Applying redactions...") + redactions = [ + { + "pageIndex": box["pageIndex"], + "boundingBox": box["boundingBox"] + } + for box in pii_boxes + ] + + # Step 3: Apply redactions to document + redacted_pdf = client.redact(pdf_file, redactions) + + # Save redacted PDF + output_file = output_folder / pdf_file.name + output_file.write_bytes(redacted_pdf) + + print(f" ✅ Redacted: {output_file.name}") + success_count += 1 + + except Exception as e: + print(f" ❌ FAILED: {e}") + failed_count += 1 - redacted = client.redact(input_path, redactions) - output_path.write_bytes(redacted) - print(f"✅ Saved to {output_path}") + # Display summary + print("=" * 60) + print(f"✅ {success_count} document(s) processed") + print(f"🔒 {total_pii_count} total PII instance(s) redacted") + if failed_count > 0: + print(f"⚠️ {failed_count} document(s) FAILED - review manually!") + print(f"📂 Output: {output_folder.absolute()}") + print("=" * 60) + + +if __name__ == "__main__": + main() From d627f26abd9b7b1bc0ccf6f48882566d9ebf8a02 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 16 Dec 2025 12:34:50 +0000 Subject: [PATCH 07/19] updated existing scripts --- samples/python/.gitignore | 1 + samples/python/README.md | 2 +- samples/python/batch_process.py | 103 ++++++++++++++++---- samples/python/bulk_password_protect.py | 100 +++++++++++++++---- samples/python/convert_cli.py | 90 ++++++++++++++--- samples/python/extract_data.py | 115 ++++++++++++++++++---- samples/python/redact_by_keyword.py | 123 ++++++++++++++++++++---- samples/python/smart_redact_pii.py | 2 +- 8 files changed, 445 insertions(+), 91 deletions(-) diff --git a/samples/python/.gitignore b/samples/python/.gitignore index 2c06782..82f2c3e 100644 --- a/samples/python/.gitignore +++ b/samples/python/.gitignore @@ -4,6 +4,7 @@ # Temporary test files and outputs temp/ output/ +test_output/ # Python __pycache__/ diff --git a/samples/python/README.md b/samples/python/README.md index 688fbb5..b95b764 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -78,7 +78,7 @@ python bulk_password_protect.py ./input ./output "MyPassword123" ```python from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient client = PlatformAPIClient() diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index 52ec9dd..9dcc87c 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -1,39 +1,102 @@ -#!/usr/bin/env python -"""Batch process documents from CLI.""" +#!/usr/bin/env python3 +""" +📁 BATCH DOCUMENT CONVERSION +============================= + +The script exemplifies a typical workflow for document format standardization. +As a IT administrator, it's essential to convert +large collections of documents into standardized formats for archival, compliance, +or system integration purposes. Manually converting individual files through desktop +applications is time-consuming and impractical for large document sets, often leading +to inconsistent results and wasted effort. + +This workflow automates bulk document conversion. The script processes files +individually - for every document matching the specified pattern in the input folder, +it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using +high-fidelity conversion algorithms. Each converted file is saved to the output +folder with the same base name but the new format extension, resulting in a +complete batch of standardized documents. + +BATCH CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, JPG, etc.) + ✓ Flexible file pattern matching (*.docx, *.xlsx, *.pptx, *.pdf.) + +USAGE: + python batch_process.py [pattern] + +EXAMPLES: + python batch_process.py ../../test_files/test-batch ./output pdf "*.docx" + python batch_process.py ./documents ./converted png "*" +""" import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient +from helper_functions.document_helpers import validate_and_setup -if __name__ == "__main__": + +# Supported output formats +SUPPORTED_FORMATS = ['pdf', 'docx', 'xlsx', 'pptx'] + + +def main(): + # Check command-line arguments if len(sys.argv) < 4: - print("Usage: python batch_process.py [pattern]") + print("Usage: python batch_process.py [pattern]") + print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") print("Example: python batch_process.py ./docs ./output pdf '*.docx'") sys.exit(1) - input_dir = Path(sys.argv[1]) - output_dir = Path(sys.argv[2]) - to_format = sys.argv[3] + # Get folder paths and format from arguments + input_folder = Path(sys.argv[1]) + output_folder = Path(sys.argv[2]) + to_format = sys.argv[3].lower() pattern = sys.argv[4] if len(sys.argv) > 4 else "*" - output_dir.mkdir(exist_ok=True) - files = list(input_dir.glob(pattern)) - - if not files: - print(f"❌ No files matching '{pattern}' found in {input_dir}") + # Validate output format + if to_format not in SUPPORTED_FORMATS: + print(f"❌ Error: Unsupported format '{to_format}'") + print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") sys.exit(1) + # Validate and setup with custom pattern + files = validate_and_setup(input_folder, output_folder, file_patterns=[pattern]) + print(f"📋 Found {len(files)} file(s) matching '{pattern}'\n") + + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - print(f"📁 Processing {len(files)} files...") + + # Process each document + success_count = 0 + failed_count = 0 for i, file_path in enumerate(files, 1): + print(f"[{i}/{len(files)}] Processing: {file_path.name}") + try: - print(f"[{i}/{len(files)}] Converting {file_path.name}...") + # Convert to target format + print(f" 🔄 Converting to {to_format.upper()}...") converted = client.convert(file_path, to_format) - output_path = output_dir / f"{file_path.stem}.{to_format}" - output_path.write_bytes(converted) - print(f" ✅ Saved to {output_path.name}") + + # Save converted file + output_file = output_folder / f"{file_path.stem}.{to_format}" + output_file.write_bytes(converted) + + print(f" ✅ Converted: {output_file.name}\n") + success_count += 1 + except Exception as e: - print(f" ❌ Error: {e}") + print(f" ❌ FAILED: {e}\n") + failed_count += 1 - print(f"✅ Batch processing complete") + # Display summary + print("=" * 60) + print(f"✅ {success_count} file(s) converted to {to_format.upper()}") + if failed_count > 0: + print(f"⚠️ {failed_count} file(s) FAILED to convert!") + print(f"📂 Output: {output_folder.absolute()}") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/samples/python/bulk_password_protect.py b/samples/python/bulk_password_protect.py index 55e34f0..f1484ae 100644 --- a/samples/python/bulk_password_protect.py +++ b/samples/python/bulk_password_protect.py @@ -1,37 +1,95 @@ -#!/usr/bin/env python -"""Password protect PDFs in bulk.""" +#!/usr/bin/env python3 +""" +🔐 BULK PASSWORD PROTECTION +============================ + +The script exemplifies a typical workflow for securing confidential documents. +As a security professional, it's essential to protect sensitive +documents with passwords before distributing them to authorized personnel, storing +them in shared drives, or archiving them for compliance purposes. Manually setting +passwords on individual files is tedious and inconsistent, leading to weak passwords +or missed files that remain unprotected. + +This workflow automates secure document protection. The script processes each PDF +file individually - for every document in the input folder, it applies robust +password encryption using a consistent password across all files. Each protected +file is saved to the output folder with the same filename, ensuring that the entire +batch of documents maintains uniform security standards. The result is a complete +set of password-protected PDFs ready for secure distribution or storage. + +DOCUMENT SECURITY STANDARDS: + ✓ Password encryption (AES-256) + ✓ Batch processing (entire folders) + ✓ Consistent security (uniform password policy) + +USAGE: + python bulk_password_protect.py + +EXAMPLE: + python bulk_password_protect.py ../../test_files/test-pdfs ./output MySecureP@ss123 +""" import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient +from helper_functions.document_helpers import validate_and_setup -if __name__ == "__main__": + +def main(): + # Check command-line arguments if len(sys.argv) != 4: - print("Usage: python bulk_password_protect.py ") + print("Usage: python bulk_password_protect.py ") sys.exit(1) - input_dir = Path(sys.argv[1]) - output_dir = Path(sys.argv[2]) + # Get folder paths and password from arguments + input_folder = Path(sys.argv[1]) + output_folder = Path(sys.argv[2]) password = sys.argv[3] - output_dir.mkdir(exist_ok=True) - files = list(input_dir.glob("*.pdf")) - - if not files: - print(f"❌ No PDF files found in {input_dir}") + # Validate password strength + if len(password) < 6: + print("❌ Error: Password must be at least 6 characters long") sys.exit(1) + # Validate and setup (only process PDF files) + files = validate_and_setup(input_folder, output_folder, file_patterns=['*.pdf']) + print(f"📋 Found {len(files)} PDF document(s) to protect\n") + + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - print(f"🔒 Protecting {len(files)} PDFs with password...") - for i, file_path in enumerate(files, 1): + # Process each document + success_count = 0 + failed_count = 0 + + for i, pdf_file in enumerate(files, 1): + print(f"[{i}/{len(files)}] Processing: {pdf_file.name}") + try: - print(f"[{i}/{len(files)}] Protecting {file_path.name}...") - protected = client.password_protect(file_path, password) - output_path = output_dir / file_path.name - output_path.write_bytes(protected) - print(f" ✅ Saved to {output_path.name}") + # Apply password protection + print(" 🔐 Applying password protection...") + protected_pdf = client.password_protect(pdf_file, password) + + # Save protected PDF + output_file = output_folder / pdf_file.name + output_file.write_bytes(protected_pdf) + + print(f" ✅ Protected: {output_file.name}\n") + success_count += 1 + except Exception as e: - print(f" ❌ Error: {e}") + print(f" ❌ FAILED: {e}\n") + failed_count += 1 - print(f"✅ Bulk password protection complete") + # Display summary + print("=" * 60) + print(f"✅ {success_count} document(s) password protected") + if failed_count > 0: + print(f"⚠️ {failed_count} document(s) FAILED - remain unprotected!") + print(f"📂 Output: {output_folder.absolute()}") + print(f"🔑 Password: {'*' * len(password)} ({len(password)} characters)") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/samples/python/convert_cli.py b/samples/python/convert_cli.py index 7cd8a9b..0df883e 100644 --- a/samples/python/convert_cli.py +++ b/samples/python/convert_cli.py @@ -1,22 +1,90 @@ -#!/usr/bin/env python -"""Document conversion from CLI.""" +#!/usr/bin/env python3 +""" +🔄 SINGLE DOCUMENT CONVERSION +============================== + +The script exemplifies a typical workflow for quick document format conversion. +As a business professional, you often need to convert individual +documents between formats for sharing, presentations, or compatibility requirements. +Whether converting a Word document to PDF for distribution, an Excel spreadsheet to +CSV for data processing, or a presentation to images for web display, manual +conversion through multiple applications is inefficient. + +This workflow provides instant document conversion. The script takes a single input +file and converts it to the specified output format using professional-grade +conversion algorithms. The result is a high-quality converted file that preserves +formatting, structure, and content fidelity, ready for immediate use. + +CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, etc.) + ✓ High-fidelity conversion (preserves formatting) + +USAGE: + python convert_cli.py + +EXAMPLES: + python convert_cli.py document.docx document.pdf pdf + python convert_cli.py presentation.pptx slide.pdf pdf + python convert_cli.py spreadsheet.xlsx data.pdf pdf +""" import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient -if __name__ == "__main__": + +# Supported output formats +SUPPORTED_FORMATS = ['pdf', 'docx', 'xlsx', 'pptx', 'png'] + + +def main(): + # Check command-line arguments if len(sys.argv) != 4: - print("Usage: python convert_cli.py ") - print("Formats: pdf, docx, xlsx, png, jpg") + print("Usage: python convert_cli.py ") + print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") + print("Example: python convert_cli.py document.docx document.pdf pdf") sys.exit(1) + # Get file paths and format from arguments input_path = Path(sys.argv[1]) output_path = Path(sys.argv[2]) - to_format = sys.argv[3] + to_format = sys.argv[3].lower() + # Validate input file exists + if not input_path.exists(): + print(f"❌ Error: Input file not found: {input_path}") + sys.exit(1) + + # Validate output format + if to_format not in SUPPORTED_FORMATS: + print(f"❌ Error: Unsupported format '{to_format}'") + print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") + sys.exit(1) + + # Create output directory if needed + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - print(f"🔄 Converting {input_path.name} to {to_format}...") - converted = client.convert(input_path, to_format) - output_path.write_bytes(converted) - print(f"✅ Saved to {output_path}") + + try: + # Convert document + print(f"🔄 Converting {input_path.name} to {to_format.upper()}...") + converted = client.convert(input_path, to_format) + + # Save converted file + output_path.write_bytes(converted) + + # Display success message + print(f"✅ Conversion successful!") + print(f"📄 Input: {input_path.name} ({input_path.stat().st_size:,} bytes)") + print(f"📄 Output: {output_path.name} ({len(converted):,} bytes)") + print(f"📂 Saved to: {output_path.absolute()}") + + except Exception as e: + print(f"❌ Conversion FAILED: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/samples/python/extract_data.py b/samples/python/extract_data.py index 433a1f5..d5502ad 100644 --- a/samples/python/extract_data.py +++ b/samples/python/extract_data.py @@ -1,31 +1,112 @@ -#!/usr/bin/env python -"""Extract forms and tables from documents.""" +#!/usr/bin/env python3 +""" +📊 DOCUMENT DATA EXTRACTION +============================ + +The script exemplifies a typical workflow for intelligent document data extraction. +As a data analyst, you need to extract structured +data from PDF documents - whether form fields from applications, surveys, and +questionnaires, or table data from reports, invoices, and financial statements. +Manual data entry is error-prone and time-consuming, especially when processing +hundreds of documents for analysis or database import. + +This workflow automates data extraction using AI-powered document understanding. +The script analyzes PDF documents and intelligently identifies and extracts either +form fields (with field names and values) or table structures (with rows, columns, +and cell contents). The extracted data is saved as structured JSON, ready for +immediate integration with databases, spreadsheets, or analytics pipelines. + +DATA EXTRACTION FEATURES: + ✓ AI-powered form field extraction + ✓ Intelligent table detection and extraction + ✓ Structured JSON output format + ✓ High accuracy recognition + +USAGE: + python extract_data.py + +MODES: + forms - Extract form fields (name-value pairs) + tables - Extract table data (rows and columns) + +EXAMPLES: + python extract_data.py forms application.pdf data.json + python extract_data.py tables invoice.pdf tables.json +""" import sys import json from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient -if __name__ == "__main__": + +def main(): + # Check command-line arguments if len(sys.argv) != 4: - print("Usage: python extract_data.py ") + print("Usage: python extract_data.py ") + print("Modes: forms, tables") + print("Example: python extract_data.py forms application.pdf data.json") sys.exit(1) - mode = sys.argv[1] + # Get mode and file paths from arguments + mode = sys.argv[1].lower() input_path = Path(sys.argv[2]) output_path = Path(sys.argv[3]) - client = PlatformAPIClient() + # Validate mode + if mode not in ['forms', 'tables']: + print("❌ Error: Mode must be 'forms' or 'tables'") + sys.exit(1) - if mode == "forms": - print(f"📋 Extracting form data from {input_path.name}...") - data = client.extract_forms(input_path) - elif mode == "tables": - print(f"📊 Extracting table data from {input_path.name}...") - data = client.extract_tables(input_path) - else: - print("❌ Mode must be 'forms' or 'tables'") + # Validate input file exists + if not input_path.exists(): + print(f"❌ Error: Input file not found: {input_path}") sys.exit(1) - output_path.write_text(json.dumps(data, indent=2)) - print(f"✅ Saved to {output_path}") + # Validate input is a PDF + if input_path.suffix.lower() != '.pdf': + print(f"❌ Error: Input must be a PDF file") + sys.exit(1) + + # Create output directory if needed + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Initialize API client (loads credentials from .env) + client = PlatformAPIClient() + + try: + # Extract data based on mode + if mode == "forms": + print(f"📋 Extracting form fields from {input_path.name}...") + data = client.extract_forms(input_path) + data_type = "form fields" + + else: # mode == "tables" + print(f"📊 Extracting table data from {input_path.name}...") + data = client.extract_tables(input_path) + data_type = "tables" + + # Count extracted items + result = data.get('result', {}) + if mode == "forms": + item_count = len(result.get('fields', [])) + else: + item_count = len(result.get('tables', [])) + + # Save extracted data as JSON + output_path.write_text(json.dumps(data, indent=2)) + + # Display success message + print(f"✅ Extraction successful!") + print(f"📊 Extracted: {item_count} {data_type}") + print(f"📄 Input: {input_path.name}") + print(f"📄 Output: {output_path.name}") + print(f"📂 Saved to: {output_path.absolute()}") + + except Exception as e: + print(f"❌ Extraction FAILED: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/samples/python/redact_by_keyword.py b/samples/python/redact_by_keyword.py index a84b81c..32fb189 100644 --- a/samples/python/redact_by_keyword.py +++ b/samples/python/redact_by_keyword.py @@ -1,35 +1,118 @@ -#!/usr/bin/env python -"""Redact by keyword search.""" +#!/usr/bin/env python3 +""" +🔍 KEYWORD-BASED REDACTION +=========================== + +The script exemplifies a typical workflow for targeted content redaction. +As a compliance officer, you need to redact specific +sensitive terms from documents before external sharing or public disclosure. +Whether removing client names, project codenames, financial figures, or +proprietary terminology, manually searching through pages and applying redactions +is tedious and risks missing instances, potentially exposing confidential information. + +This workflow automates keyword-based redaction. The script searches the entire +PDF document for all specified keywords and phrases, identifies their exact +locations across all pages, then automatically applies permanent redactions to +remove them. Multiple keywords can be processed in a single pass, ensuring +comprehensive coverage. The result is a thoroughly redacted document ready for +safe distribution. + +KEYWORD REDACTION FEATURES: + ✓ Multi-keyword search (process multiple terms) + ✓ Whole document scanning (all pages) + ✓ Exact location detection + ✓ Permanent redaction (unrecoverable) + ⏳ Case-insensitive matching (feature in development) + ⏳ Regex pattern support (feature in development) + +USAGE: + python redact_by_keyword.py [keyword2 ...] + +EXAMPLES: + python redact_by_keyword.py contract.pdf redacted.pdf "confidential" "proprietary" + python redact_by_keyword.py report.pdf clean.pdf "Project Zeus" "Client ABC" +""" import sys from pathlib import Path -from platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient -if __name__ == "__main__": + +def main(): + # Check command-line arguments if len(sys.argv) < 4: - print("Usage: python redact_by_keyword.py [keyword2 ...]") + print("Usage: python redact_by_keyword.py [keyword2 ...]") + print("Example: python redact_by_keyword.py document.pdf redacted.pdf 'confidential' 'secret'") sys.exit(1) + # Get file paths and keywords from arguments input_path = Path(sys.argv[1]) output_path = Path(sys.argv[2]) keywords = sys.argv[3:] - client = PlatformAPIClient() - - print(f"🔍 Finding keywords in {input_path.name}...") - bbox_data = client.find_text_boxes(input_path, keywords) + # Validate input file exists + if not input_path.exists(): + print(f"❌ Error: Input file not found: {input_path}") + sys.exit(1) - text_boxes = bbox_data.get('result', {}).get('textBoxes', []) - if not text_boxes: - print("✅ No keywords found") - sys.exit(0) + # Validate input is a PDF + if input_path.suffix.lower() != '.pdf': + print(f"❌ Error: Input must be a PDF file") + sys.exit(1) - print(f"🎯 Found {len(text_boxes)} keyword instances") - print(f"🔒 Redacting keywords...") + # Create output directory if needed + output_path.parent.mkdir(parents=True, exist_ok=True) - redactions = [{"pageIndex": box["pageIndex"], "boundingBox": box["boundingBox"]} - for box in text_boxes] + # Initialize API client (loads credentials from .env) + client = PlatformAPIClient() - redacted = client.redact(input_path, redactions) - output_path.write_bytes(redacted) - print(f"✅ Saved to {output_path}") + try: + # Step 1: Search for keywords in document + print(f"🔍 Searching for {len(keywords)} keyword(s) in {input_path.name}...") + print(f" Keywords: {', '.join(repr(k) for k in keywords)}") + + bbox_data = client.find_text_boxes(input_path, keywords) + + # Extract text box locations from response + text_boxes = bbox_data.get('result', {}).get('textBoxes', []) + + if not text_boxes: + print("ℹ️ No keyword matches found - copying original file") + # Copy original file to output if no keywords found + output_path.write_bytes(input_path.read_bytes()) + print(f"✅ Saved: {output_path.name}") + print(f"📂 Output: {output_path.absolute()}") + return + + print(f"🎯 Found {len(text_boxes)} keyword instance(s) to redact") + + # Step 2: Prepare redaction coordinates + print("🔒 Applying redactions...") + redactions = [ + { + "pageIndex": box["pageIndex"], + "boundingBox": box["boundingBox"] + } + for box in text_boxes + ] + + # Step 3: Apply redactions to document + redacted_pdf = client.redact(input_path, redactions) + + # Save redacted PDF + output_path.write_bytes(redacted_pdf) + + # Display success message + print(f"✅ Redaction successful!") + print(f"🔒 Redacted: {len(text_boxes)} instance(s)") + print(f"📄 Input: {input_path.name}") + print(f"📄 Output: {output_path.name}") + print(f"📂 Saved to: {output_path.absolute()}") + + except Exception as e: + print(f"❌ Redaction FAILED: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/samples/python/smart_redact_pii.py b/samples/python/smart_redact_pii.py index 052bc18..83fd1fd 100644 --- a/samples/python/smart_redact_pii.py +++ b/samples/python/smart_redact_pii.py @@ -4,7 +4,7 @@ ====================== The script exemplifies a typical workflow for protecting sensitive customer information. -As a compliance officer or legal professional, it's essential to review and redact +As a compliance officer, it's essential to review and redact personally identifiable information (PII) from documents before sharing them with third parties, storing them in public systems, or using them for analysis. Manual redaction is time-consuming and error-prone, potentially missing sensitive data like From ffa6809efacd0ff7b7a3b6299c3591be09cc0a66 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Thu, 18 Dec 2025 01:16:50 +0000 Subject: [PATCH 08/19] creation of sign workflow (status:working but needs cleanup --- postman/Sign-API.postman_collection.json | 604 ++++++++++++++++ samples/python/README.md | 18 +- samples/python/Taskfile.yml | 16 + samples/python/api/sign_api.py | 316 +++++++++ samples/python/requirements.txt | 1 + samples/python/send_policies_to_employees.py | 655 ++++++++++++++++++ test_files/test-sign/README.md | 63 ++ test_files/test-sign/company-policies.pdf | Bin 0 -> 139766 bytes .../test-sign/confidentiality-agreement.pdf | Bin 0 -> 75673 bytes test_files/test-sign/employees.csv | 5 + .../test-sign/sample-company-policies.pdf | Bin 0 -> 139766 bytes 11 files changed, 1676 insertions(+), 2 deletions(-) create mode 100755 postman/Sign-API.postman_collection.json create mode 100644 samples/python/api/sign_api.py create mode 100644 samples/python/send_policies_to_employees.py create mode 100644 test_files/test-sign/README.md create mode 100755 test_files/test-sign/company-policies.pdf create mode 100755 test_files/test-sign/confidentiality-agreement.pdf create mode 100644 test_files/test-sign/employees.csv create mode 100755 test_files/test-sign/sample-company-policies.pdf diff --git a/postman/Sign-API.postman_collection.json b/postman/Sign-API.postman_collection.json new file mode 100755 index 0000000..3598002 --- /dev/null +++ b/postman/Sign-API.postman_collection.json @@ -0,0 +1,604 @@ +{ + "info": { + "_postman_id": "79ced7f0-a852-414f-ac60-0c762a02263b", + "name": "Nitro Collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "23623766" + }, + "item": [ + { + "name": "Envelope", + "item": [ + { + "name": "Document", + "item": [ + { + "name": "List Documents by Envelope ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{baseURL}}/sign/envelopes//documents", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "documents" + ] + } + }, + "response": [] + }, + { + "name": "Get Document by ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{baseURL}}/sign/envelopes//documents/", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "documents", + "" + ] + } + }, + "response": [] + }, + { + "name": "Delete Document by ID", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{baseURL}}/sign/envelopes//documents/", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "documents", + "" + ] + } + }, + "response": [] + }, + { + "name": "Create Document by Envelope ID", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "metadata", + "value": "{\n \"name\": \"Sample doc\"\n}", + "type": "text" + }, + { + "key": "payload", + "type": "file", + "src": "example-file.pdf" + } + ] + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes//documents", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "documents" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Fields", + "item": [ + { + "name": "Get Document by ID Copy", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"boundingBox\": [0, 0, 200, 40],\n \"participantID\": \"/documents//fields", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "documents", + "", + "fields" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Participant", + "item": [ + { + "name": "Create Participant by Envelope ID", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"role\": \"\",\n \"email\": \"\",\n \"authentication\": {\n \"type\": \"AccessCode\",\n \"accessCode\": \"\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes//participants", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "participants" + ] + } + }, + "response": [] + }, + { + "name": "List Participants by Envelope ID", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{baseURL}}/sign/envelopes//participants", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "", + "participants" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "List Envelope", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseURL}}/sign/envelopes", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes" + ], + "query": [ + { + "key": "pageAfter", + "value": "", + "disabled": true + }, + { + "key": "pageBefore", + "value": "", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Get Envelope by ID", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes/", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "" + ] + } + }, + "response": [] + }, + { + "name": "Create Envelope", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"\",\n \"mode\": \"\",\n \"notification\": {\n \"subject\": \"\",\n \"body\": \"\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes" + ] + } + }, + "response": [] + }, + { + "name": "Update Envelope", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"\",\n \"mode\": \"\",\n \"notification\": {\n \"subject\": \"\",\n \"body\": \"\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes/", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + "" + ] + } + }, + "response": [] + }, + { + "name": "Download Sealed Envelope", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes/:download-sealed", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + ":download-sealed" + ] + } + }, + "response": [] + }, + { + "name": "Download Original Envelope", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseURL}}/sign/envelopes/:download-original", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "envelopes", + ":download-original" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Authentication", + "item": [ + { + "name": "Get Access Token", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"cliendID\": \"\",\n \"clientSecret\": \"\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/oauth/token", + "host": [ + "{{baseURL}}" + ], + "path": [ + "oauth", + "token" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Conversions", + "item": [ + { + "name": "Get Converted File by Job ID", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseURL}}/sign/conversions/:download-converted", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "conversions", + ":download-converted" + ] + } + }, + "response": [] + }, + { + "name": "Get Conversion Job Status", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [] + }, + "url": { + "raw": "{{baseURL}}/sign/conversions//status", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "conversions", + "", + "status" + ] + } + }, + "response": [] + }, + { + "name": "Convert File to PDF", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "example-file.pdf" + } + ] + }, + "url": { + "raw": "{{baseURL}}/sign/conversions", + "host": [ + "{{baseURL}}" + ], + "path": [ + "sign", + "conversions" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "baseURL", + "value": "https://api.gonitro.dev" + }, + { + "key": "token", + "value": "" + } + ] +} \ No newline at end of file diff --git a/samples/python/README.md b/samples/python/README.md index b95b764..1c49dad 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -20,9 +20,17 @@ Python examples for integrating with the Nitro Platform API. python quickstart.py ``` -## Files +## API Clients -- `platform_api.py` - Main API client library +- `api/platform_api.py` - Platform API client (conversions, extractions, transformations) +- `api/sign_api.py` - Sign API client (eSignature/envelopes) - **NEW!** + +See [SIGN_API.md](SIGN_API.md) for detailed Sign API documentation. + +## CLI Tools + +### Platform API Tools +- `platform_api.py` - Main API client library (legacy location) - `quickstart.py` - Authentication test - `convert_cli.py` - Document conversion - `extract_data.py` - Extract forms and tables @@ -31,6 +39,12 @@ Python examples for integrating with the Nitro Platform API. - `batch_process.py` - Batch convert documents - `bulk_password_protect.py` - Password protect multiple PDFs +### Sign API Tools (eSignature) +- `send_policies_to_employees.py` - Send multiple policy documents to multiple employees for signature + +### Complete Workflow Examples +- `prepare_pdf_for_distribution.py` - Prepare PDFs for external distribution (convert, compress, remove metadata) + ## Usage Examples ### Authentication diff --git a/samples/python/Taskfile.yml b/samples/python/Taskfile.yml index 3e3fc49..a303a09 100644 --- a/samples/python/Taskfile.yml +++ b/samples/python/Taskfile.yml @@ -53,3 +53,19 @@ tasks: cmds: - cp .env.example .env - echo "✅ Created .env file - please update with your credentials" + + # ========== Sign API Tasks (eSignature) ========== + + send-policies: + desc: "Send policy PDF to multiple employees for signature (e.g., task send-policies PDF=policies.pdf CSV=employees.csv OUTPUT=./signed)" + cmds: + - python send_policies_to_employees.py {{.PDF}} {{.CSV}} {{.OUTPUT}} + requires: + vars: [PDF, CSV, OUTPUT] + + send-policies-no-wait: + desc: "Send policy PDF to all employees without waiting (e.g., task send-policies-no-wait PDF=policies.pdf CSV=employees.csv OUTPUT=./signed)" + cmds: + - python send_policies_to_employees.py {{.PDF}} {{.CSV}} {{.OUTPUT}} --no-wait + requires: + vars: [PDF, CSV, OUTPUT] diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py new file mode 100644 index 0000000..1418e32 --- /dev/null +++ b/samples/python/api/sign_api.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python +"""Sign API client for Nitro Sign integrations (eSignature operations).""" + +import os +import time +import json +import requests +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any +from dotenv import load_dotenv + +load_dotenv() + + +@dataclass +class SignAPIClient: + """Synchronous client for Nitro Sign API operations (eSignature/envelopes).""" + + base_url: str = field(default_factory=lambda: os.getenv('PLATFORM_BASE_URL', 'https://api.gonitro.dev')) + client_id: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_ID')) + client_secret: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_SECRET')) + _token: str | None = field(default=None, init=False) + _token_expiry: float = field(default=0, init=False) + + def _get_token(self) -> str: + """Get or refresh OAuth2 access token.""" + if self._token and time.time() < self._token_expiry: + return self._token + + response = requests.post( + f'{self.base_url}/oauth/token', + json={'clientID': self.client_id, 'clientSecret': self.client_secret} + ) + response.raise_for_status() + data = response.json() + self._token = data['accessToken'] + self._token_expiry = time.time() + data.get('expiresIn', 3600) - 60 + return self._token + + def _request( + self, + method: str, + endpoint: str, + json_data: dict[str, Any] = None, + params: dict[str, Any] = None + ) -> dict[str, Any]: + """Make authenticated API request returning JSON.""" + headers = {'Authorization': f'Bearer {self._get_token()}'} + + response = requests.request( + method=method, + url=f'{self.base_url}{endpoint}', + headers=headers, + json=json_data, + params=params + ) + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + # Try to get error details from response + try: + error_detail = response.json() + print(f' ❌ API Error Response: {error_detail}') + except: + print(f' ❌ API Error (no JSON): {response.text}') + raise e + + return response.json() + + def _request_bytes( + self, + method: str, + endpoint: str, + params: dict[str, Any] = None + ) -> bytes: + """Make authenticated API request returning binary data.""" + headers = {'Authorization': f'Bearer {self._get_token()}'} + + response = requests.request( + method=method, + url=f'{self.base_url}{endpoint}', + headers=headers, + params=params + ) + + response.raise_for_status() + return response.content + + # ========== Envelope Management ========== + + def list_envelopes( + self, + page_after: str = None, + page_before: str = None + ) -> dict[str, Any]: + """List all envelopes with cursor-based pagination. + + Args: + page_after: Cursor token to get items after the last item from previous response + page_before: Cursor token to get items before the last item from previous response + + Returns: + Dict with 'items' (list of envelopes) and optional 'nextPage' (cursor token) + """ + params = {} + if page_after: + params['pageAfter'] = page_after + elif page_before: + params['pageBefore'] = page_before + + return self._request('GET', '/sign/envelopes', params=params) + + def create_envelope(self, envelope_data: dict[str, Any]) -> dict[str, Any]: + """Create a new envelope. + + Args: + envelope_data: Envelope configuration including name, documents, participants, fields + + Returns: + Created envelope with ID and status + """ + return self._request('POST', '/sign/envelopes', json_data=envelope_data) + + def get_envelope(self, envelope_id: str) -> dict[str, Any]: + """Get envelope details by ID. + + Args: + envelope_id: UUID of the envelope + + Returns: + Envelope details including status, participants, documents + """ + return self._request('GET', f'/sign/envelopes/{envelope_id}') + + def update_envelope(self, envelope_id: str, updates: dict[str, Any]) -> dict[str, Any]: + """Update an envelope. + + Args: + envelope_id: UUID of the envelope + updates: Fields to update (name, participants, etc.) + + Returns: + Updated envelope data + """ + return self._request('PATCH', f'/sign/envelopes/{envelope_id}', json_data=updates) + + def delete_envelope(self, envelope_id: str) -> None: + """Delete an envelope by ID. + + Args: + envelope_id: UUID of the envelope + """ + headers = {'Authorization': f'Bearer {self._get_token()}'} + response = requests.delete( + f'{self.base_url}/sign/envelopes/{envelope_id}', + headers=headers + ) + response.raise_for_status() + + # ========== Document Management ========== + + def create_document( + self, + envelope_id: str, + file_path: Path, + document_name: str = None + ) -> dict[str, Any]: + """Upload a document to an envelope using form-data. + + Args: + envelope_id: ID of the envelope + file_path: Path to the PDF file to upload + document_name: Optional custom name for the document + + Returns: + Created document with ID + """ + if document_name is None: + document_name = file_path.name + + # Read the binary content of the PDF file + with open(file_path, 'rb') as f: + pdf_binary = f.read() + + # Prepare metadata as JSON string + import json + metadata = json.dumps({'name': document_name}) + + # Prepare form-data with binary content + files = { + 'metadata': ('metadata', metadata, 'application/json'), + 'payload': (file_path.name, pdf_binary, 'application/pdf') + } + + headers = {'Authorization': f'Bearer {self._get_token()}'} + + response = requests.post( + f'{self.base_url}/sign/envelopes/{envelope_id}/documents', + headers=headers, + files=files + ) + + response.raise_for_status() + return response.json() + + # ========== Participant Management ========== + + def create_participant( + self, + envelope_id: str, + participant_data: dict[str, Any] + ) -> dict[str, Any]: + """Add a participant to an envelope. + + Args: + envelope_id: ID of the envelope + participant_data: Participant configuration with role, email, name + + Returns: + Created participant with ID + """ + return self._request( + 'POST', + f'/sign/envelopes/{envelope_id}/participants', + json_data=participant_data + ) + + # ========== Field Management ========== + + def create_field( + self, + envelope_id: str, + document_id: str, + field_data: dict[str, Any] + ) -> dict[str, Any]: + """Add a signature field to a document in an envelope. + + Args: + envelope_id: ID of the envelope + document_id: ID of the document + field_data: Field configuration with boundingBox, participantID, type, page + + Returns: + Created field with ID + """ + return self._request( + 'POST', + f'/sign/envelopes/{envelope_id}/documents/{document_id}/fields', + json_data=field_data + ) + + # ========== Envelope Actions ========== + + def send_for_signing(self, envelope_id: str) -> dict[str, Any]: + """Send envelope to participants for signing. + + This transitions the envelope from 'drafted' to 'sent' status. + + Args: + envelope_id: UUID of the envelope + + Returns: + Updated envelope with 'sent' status + """ + # The correct endpoint uses a colon before 'send-for-signing' + return self._request('POST', f'/sign/envelopes/{envelope_id}:send-for-signing') + + def cancel_envelope(self, envelope_id: str) -> dict[str, Any]: + """Cancel an envelope that was sent for signing. + + Args: + envelope_id: UUID of the envelope + + Returns: + Envelope with 'cancelled' status + """ + return self._request('PUT', f'/sign/envelopes/{envelope_id}/cancel') + + def send_reminders(self, envelope_id: str) -> dict[str, Any]: + """Send reminder notifications to pending signers. + + Args: + envelope_id: UUID of the envelope + + Returns: + Confirmation of reminder sent + """ + return self._request('POST', f'/sign/envelopes/{envelope_id}/reminders') + + # ========== Document Downloads ========== + + def download_sealed_envelope(self, envelope_id: str) -> bytes: + """Download the sealed (signed and completed) envelope. + + Args: + envelope_id: UUID of the envelope + + Returns: + PDF bytes of the sealed document + """ + # The correct endpoint uses a colon before 'download-sealed' + return self._request_bytes('GET', f'/sign/envelopes/{envelope_id}:download-sealed') + + def download_original_envelope(self, envelope_id: str) -> bytes: + """Download the original (unsigned) envelope documents. + + Args: + envelope_id: UUID of the envelope + + Returns: + Original document bytes + """ + # The correct endpoint uses a colon before 'download-original' + return self._request_bytes('GET', f'/sign/envelopes/{envelope_id}:download-original') diff --git a/samples/python/requirements.txt b/samples/python/requirements.txt index 2b1be2a..da9bf17 100644 --- a/samples/python/requirements.txt +++ b/samples/python/requirements.txt @@ -1,2 +1,3 @@ requests>=2.31.0 python-dotenv>=1.0.0 +reportlab>=4.0.0 diff --git a/samples/python/send_policies_to_employees.py b/samples/python/send_policies_to_employees.py new file mode 100644 index 0000000..219b437 --- /dev/null +++ b/samples/python/send_policies_to_employees.py @@ -0,0 +1,655 @@ +#!/usr/bin/env python3 +""" +📝 SEND POLICY DOCUMENTS TO MULTIPLE EMPLOYEES +=============================================== + +This script sends multiple policy documents from a folder to multiple employees for their +signature and saves each signed copy in a dedicated folder per employee. + +WORKFLOW: + 1. Load all policy documents from a folder (PDF, Word documents) + 2. Convert non-PDF documents to PDF automatically + 3. Read employee list from CSV file (name, email) + 4. For each employee: + - Create signature envelope with ALL policy documents + - Send for electronic signature + - Monitor until signed + - Save signed documents as ZIP (contains all signed PDFs + audit trail) + +EMPLOYEE CSV FORMAT: + name,email + John Doe,john.doe@company.com + Jane Smith,jane.smith@company.com + Bob Johnson,bob.johnson@company.com + +USAGE: + python send_policies_to_employees.py + +EXAMPLE: + python send_policies_to_employees.py ./policies ./employees.csv ./newJoinersJanuary + +OUTPUT STRUCTURE: + newJoinersJanuary/ + ├── john-doe/ + │ ├── signed-policies.zip # ZIP file with all signed documents + │ ├── signed-documents/ # Extracted contents: + │ │ ├── policy1-signed.pdf # - Signed policy documents + │ │ ├── policy2-signed.pdf + │ │ └── audit-trail.pdf # - Audit trail document + │ ├── envelope-info.json + │ └── envelope-id.txt + ├── jane-smith/ + │ ├── signed-policies.zip + │ ├── signed-documents/ + │ ├── envelope-info.json + │ └── envelope-id.txt + └── bob-johnson/ + ├── signed-policies.zip + ├── signed-documents/ + ├── envelope-info.json + └── envelope-id.txt + +NOTE: The Nitro Sign API returns signed envelopes as ZIP files containing all signed PDFs + plus an audit trail. The script automatically extracts the ZIP for convenience. +""" + +import sys +import csv +import time +import json +import base64 +from pathlib import Path +from api.sign_api import SignAPIClient + + +# def load_employees_from_csv(csv_path: Path) -> list[dict]: +# """Load employee list from CSV file. + +# Args: +# csv_path: Path to CSV file with columns: name, email + +# Returns: +# List of employee dictionaries with 'name' and 'email' +# """ +# employees = [] + +# with open(csv_path, 'r', encoding='utf-8') as f: +# reader = csv.DictReader(f) + +# # Validate CSV has required columns +# if 'name' not in reader.fieldnames or 'email' not in reader.fieldnames: +# raise ValueError('CSV must have "name" and "email" columns') + +# for row in reader: +# name = row['name'].strip() +# email = row['email'].strip() + +# if name and email and '@' in email: +# employees.append({'name': name, 'email': email}) +# else: +# print(f' ⚠️ Skipping invalid row: {row}') + +# return employees + + +def create_employee_folder_name(employee_name: str) -> str: + """Convert employee name to folder-safe name. + + Args: + employee_name: Full name like "John Doe" + + Returns: + Folder-safe name like "john-doe" + """ + return employee_name.lower().replace(' ', '-').replace('.', '') + +def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: + + print(f'📂 Loading policy documents from: {policies_folder}') + + # Find only PDF files (no conversion for now, keep it simple) + policy_files = list(policies_folder.glob('*.pdf')) + + if not policy_files: + raise ValueError(f'No PDF files found in {policies_folder}') + + print(f' ✅ Found {len(policy_files)} PDF document(s)') + + documents = [] + for pf in policy_files: + print(f' • {pf.name}') + + # Read binary content + with open(pf, 'rb') as f: + binary_data = f.read() + + documents.append({ + 'name': pf.name, + 'binary': binary_data, + 'path': str(pf) # Convert PosixPath to string + }) + + print() + return documents + + +def create_signature_envelope( + sign_client: SignAPIClient, + documents: list[Path], # List of file paths + employee_name: str, + employee_email: str +) -> tuple[str, list[str]]: # Returns envelope_id and list of document_ids + """Create envelope and upload documents. + + Args: + sign_client: Sign API client instance + documents: List of Path objects to PDF files + employee_name: Full name of employee + employee_email: Email address of employee + + Returns: + Tuple of (envelope_id, list of document_ids) + """ + + # ============================================================ + # STEP 1: Create empty envelope + # ============================================================ + print(f' 📝 Step 1: Creating empty envelope...') + + envelope_data = { + 'name': f'Company Policies - {employee_name}', + 'mode': "parallel", + 'notification': { + 'subject': f'Please sign: Company Policies', + 'body': f'Hello {employee_name}, please review and sign the attached company policy documents.' + } + } + print(f' 🔍 DEBUG - Sending: {envelope_data}') + + envelope = sign_client.create_envelope(envelope_data) + envelope_id = envelope['ID'] + print(f' ✅ Envelope created: {envelope_id}') + + # ============================================================ + # STEP 2: Upload documents to envelope + # ============================================================ + print(f'\n 📄 Step 2: Uploading {len(documents)} document(s)...') + document_ids = [] + + for i, doc in enumerate(documents, 1): + doc_name = doc['name'] + doc_binary = doc['binary'] + doc_path = doc['path'] + + print(f' [{i}/{len(documents)}] Uploading: {doc_name}') + + # Prepare metadata as JSON string + import json + metadata = json.dumps({'name': doc_name}) + + # Prepare form-data with binary content + files = { + 'metadata': ('metadata', metadata, 'application/json'), + 'payload': (doc_name, doc_binary, 'application/pdf') + } + + headers = {'Authorization': f'Bearer {sign_client._get_token()}'} + + import requests + response = requests.post( + f'{sign_client.base_url}/sign/envelopes/{envelope_id}/documents', + headers=headers, + files=files + ) + + response.raise_for_status() + document = response.json() + + document_id = document['ID'] + document_ids.append(document_id) + print(f' ✅ Uploaded: {document_id}') + + return envelope_id, document_ids + + + +def monitor_envelope(sign_client: SignAPIClient, envelope_id: str, timeout_minutes: int = 60) -> str: + """Monitor envelope until signed, cancelled, or timeout. + + Args: + sign_client: Sign API client instance + envelope_id: ID of envelope to monitor + timeout_minutes: Maximum time to wait + + Returns: + Final status: 'sealed', 'cancelled', 'timeout', or 'error' + """ + check_interval = 30 # seconds + max_checks = (timeout_minutes * 60) // check_interval + + for i in range(max_checks): + try: + envelope = sign_client.get_envelope(envelope_id) + status = envelope['status'] + + if status == 'sealed': + return 'sealed' + elif status in ['cancelled', 'rejected', 'deleted']: + return 'cancelled' + + if i < max_checks - 1: + time.sleep(check_interval) + + except Exception as e: + print(f' ⚠️ Error checking status: {e}') + return 'error' + + return 'timeout' + + +def download_signed_document( + sign_client: SignAPIClient, + envelope_id: str, + output_folder: Path, + document_name: str +) -> Path: + """Download sealed envelope to employee folder. + + The API returns a ZIP file containing: + - All signed PDF documents + - Audit trail document + + Args: + sign_client: Sign API client instance + envelope_id: ID of sealed envelope + output_folder: Employee-specific output folder + document_name: Name for the saved file (should end with .zip) + + Returns: + Path to saved ZIP file + """ + # Download sealed envelope (returns ZIP file) + zip_bytes = sign_client.download_sealed_envelope(envelope_id) + + # Save as ZIP file + if not document_name.endswith('.zip'): + document_name = document_name.replace('.pdf', '.zip') + + output_path = output_folder / document_name + output_path.write_bytes(zip_bytes) + + # Also extract the ZIP contents for convenience + import zipfile + extract_folder = output_folder / 'signed-documents' + extract_folder.mkdir(exist_ok=True) + + with zipfile.ZipFile(output_path, 'r') as zip_ref: + zip_ref.extractall(extract_folder) + + print(f' 📦 ZIP saved to: {output_path}') + print(f' 📂 Extracted to: {extract_folder}') + + # Save envelope metadata + envelope = sign_client.get_envelope(envelope_id) + json_path = output_folder / 'envelope-info.json' + json_path.write_text(json.dumps(envelope, indent=2)) + + return output_path + + +def process_employee( + sign_client: SignAPIClient, + documents: list[str], + employee: dict, + base_output_folder: Path, + timeout_minutes: int, + wait_for_signatures: bool +) -> dict: + """Process signature request for one employee with multiple documents. + + Args: + sign_client: Sign API client instance + documents: List of policy document dictionaries + employee: Employee dict with 'name' and 'email' + base_output_folder: Base folder for all signed documents + timeout_minutes: How long to wait for signature + wait_for_signatures: If True, wait for signature; if False, just send + + Returns: + Result dictionary with status and details + """ + name = employee['name'] + email = employee['email'] + + print(f'\n📧 Processing: {name} ({email})') + print(f' {"─" * 60}') + + try: + # Create employee-specific output folder + folder_name = create_employee_folder_name(name) + employee_folder = base_output_folder / folder_name + employee_folder.mkdir(parents=True, exist_ok=True) + + # Documents are already loaded and passed as parameter + # STEP 1 & 2: Create envelope and upload documents + print(f' 📝 Creating envelope for {name}...') + envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) + print(f' ✅ Envelope created: {envelope_id}') + print(f' ✅ Documents uploaded: {len(document_ids)}') + for i, doc_id in enumerate(document_ids, 1): + print(f' [{i}] {doc_id}') + + # ============================================================ + # STEP 3: Add participant (signer) + # ============================================================ + print(f'\n 👤 Step 3: Adding participant...') + + participant_data = { + 'email': email, + 'role': 'signer', + 'name': name + } + + print(f' 🔍 DEBUG - Participant data: {participant_data}') + + participant = sign_client.create_participant(envelope_id, participant_data) + participant_id = participant['ID'] + print(f' ✅ Participant added: {participant_id}') + + # ============================================================ + # STEP 4: Add signature fields to each document + # ============================================================ + print(f'\n ✍️ Step 4: Adding signature fields...') + + for i, doc_id in enumerate(document_ids, 1): + print(f' [{i}/{len(document_ids)}] Adding fields for document {doc_id}') + + # Add signature field + signature_field_data = { + 'participantID': participant_id, + 'type': 'signature', + 'label': 'Your Signature', + 'page': 1, + 'boundingBox': [200, 300, 60, 40], # [x, y, width, height] + 'required': True + } + + signature_field = sign_client.create_field(envelope_id, doc_id, signature_field_data) + print(f' ✅ Signature field: {signature_field["ID"]}') + + # Add date field + date_field_data = { + 'participantID': participant_id, + 'type': 'date', + 'label': 'Date Signed', + 'page': 1, + 'boundingBox': [320, 650, 150, 50], # [x, y, width, height] + 'required': True, + 'format': 'MM/DD/YYYY' + } + + date_field = sign_client.create_field(envelope_id, doc_id, date_field_data) + print(f' ✅ Date field: {date_field["ID"]}') + + + envelope = sign_client.get_envelope(envelope_id) + print(f"!!!!!!!!!!!!!!!!!Envelope status: {envelope['status']}") + # ============================================================ + # STEP 5: Send envelope for signing + # ============================================================ + print(f'\n 📤 Step 5: Sending envelope for signing...') + sign_client.send_for_signing(envelope_id) + print(f' ✅ Envelope sent to {email}') + envelope = sign_client.get_envelope(envelope_id) + + print(f"!!!!!!!!!!!!!!!!!Envelope status: {envelope['status']}") + + # ============================================================ + # STEP 6: Monitor status and download signed document + # ============================================================ + if wait_for_signatures: + print(f'\n ⏳ Step 6: Monitoring signing status...') + print(f' 💡 Employee will receive email at {email}') + print(f' ⏱️ Checking every 30 seconds (timeout: {timeout_minutes} min)...') + + status = monitor_envelope(sign_client, envelope_id, timeout_minutes) + + if status == 'sealed': + print(f' ✅ Document signed!') + print(f' 📥 Downloading signed document...') + + output_path = download_signed_document( + sign_client, + envelope_id, + employee_folder, + 'signed-policies.pdf' + ) + + print(f' 💾 Saved to: {output_path}') + + return { + 'status': 'success', + 'name': name, + 'email': email, + 'envelope_id': envelope_id, + 'output_path': str(output_path), + 'num_documents': len(document_ids) + } + elif status == 'timeout': + print(f' ⏱️ Timeout - not signed yet') + return { + 'status': 'timeout', + 'name': name, + 'email': email, + 'envelope_id': envelope_id, + 'folder': str(employee_folder), + 'num_documents': len(document_ids) + } + elif status == 'cancelled': + print(f' ❌ Envelope cancelled or rejected') + return { + 'status': 'cancelled', + 'name': name, + 'email': email, + 'envelope_id': envelope_id + } + else: + print(f' ❌ Error monitoring envelope') + return { + 'status': 'error', + 'name': name, + 'email': email, + 'envelope_id': envelope_id + } + else: + # Not waiting for signatures + print(f' ✅ Envelope sent (not waiting for signature)') + return { + 'status': 'sent', + 'name': name, + 'email': email, + 'envelope_id': envelope_id, + 'folder': str(employee_folder), + 'num_documents': len(documents) + } + + + + except Exception as e: + print(f' ❌ Error: {e}') + return { + 'status': 'failed', + 'name': name, + 'email': email, + 'error': str(e) + } + + +def display_summary(results: list[dict], wait_for_signatures: bool): + """Display final summary of all operations. + + Args: + results: List of result dictionaries + wait_for_signatures: Whether signatures were waited for + """ + print('\n' + '=' * 70) + print('📊 SUMMARY') + print('=' * 70) + + success = [r for r in results if r['status'] == 'success'] + created = [r for r in results if r['status'] == 'created'] # NEW: for Step 1 testing + sent = [r for r in results if r['status'] == 'sent'] + timeout = [r for r in results if r['status'] == 'timeout'] + cancelled = [r for r in results if r['status'] == 'cancelled'] + failed = [r for r in results if r['status'] == 'failed'] + + # TESTING MODE: Show created envelopes + print(f'✅ Envelopes created (Step 1): {len(created)}') + print(f'✅ Completed and signed: {len(success)}') + print(f'📤 Sent (not waiting): {len(sent)}') + print(f'⏱️ Timeout: {len(timeout)}') + print(f'❌ Cancelled/Rejected: {len(cancelled)}') + print(f'❌ Failed: {len(failed)}') + + print() + + # Show created envelopes (Step 1 testing) + if created: + print('✅ Envelopes created (testing Step 1):') + for r in created: + num_docs = r.get('num_documents', 0) + print(f" • {r['name']}: envelope {r['envelope_id']} ({num_docs} documents pending)") + print(f" Folder: {r['folder']}") + print() + + # Show successful completions + if success: + print('✅ Signed documents saved to:') + for r in success: + num_docs = r.get('num_documents', 0) + print(f" • {r['name']}: {r['output_path']} ({num_docs} documents)") + print() + + # Show timeouts + if timeout: + print('⏱️ Not signed yet (check these later):') + for r in timeout: + print(f" • {r['name']}: envelope {r['envelope_id']}") + print(f" Command: python download_envelope.py {r['envelope_id']} {r['folder']}/signed-policies.pdf") + print() + + # Show sent (not waiting) + if sent: + print('📤 Envelopes sent (check status later):') + for r in sent: + num_docs = r.get('num_documents', 0) + print(f" • {r['name']}: envelope {r['envelope_id']} ({num_docs} documents)") + print(f" Folder: {r['folder']}") + print() + + # Show failures + if failed: + print('❌ Failed to process:') + for r in failed: + print(f" • {r['name']} ({r['email']}): {r.get('error', 'Unknown error')}") + print() + + print('=' * 70) + + +def main(): + # Check command-line arguments + if len(sys.argv) < 4: + print('Usage: python send_policies_to_employees.py [--no-wait]') + sys.exit(1) + + # Parse arguments + policies_folder = Path(sys.argv[1]) + employees_csv = Path(sys.argv[2]) + output_folder = Path(sys.argv[3]) + wait_for_signatures = '--no-wait' not in sys.argv + + # Validate inputs + if not policies_folder.exists() or not policies_folder.is_dir(): + print(f'❌ Policies folder not found: {policies_folder}') + sys.exit(1) + + if not employees_csv.exists(): + print(f'❌ Employees CSV not found: {employees_csv}') + sys.exit(1) + + # Display header + print('=' * 70) + print('📝 SEND POLICY DOCUMENTS TO MULTIPLE EMPLOYEES') + print('=' * 70) + print(f'Policies folder: {policies_folder}') + print(f'Employees list: {employees_csv}') + print(f'Output folder: {output_folder}') + print(f'Wait for signatures: {"Yes" if wait_for_signatures else "No (send only)"}') + print('=' * 70) + print() + + try: + # Load employees + print('👥 Loading employees from CSV...') + employees = [{'name': "Isadora", 'email': "isamap2410@gmail.com"}, ] + # {'name': "John Doe", 'email': "john.doe@example.com"} + + + # load_employees_from_csv(employees_csv) + print(f' ✅ Found {len(employees)} employee(s)') + + if len(employees) == 0: + print('❌ No valid employees found in CSV') + sys.exit(1) + + # Load and prepare all policy documents + print() + documents = load_policy_documents_from_folder(policies_folder) + + # Create output folder + output_folder.mkdir(parents=True, exist_ok=True) + + # Initialize Sign API client + sign_client = SignAPIClient() + + # Process each employee + print() + print('=' * 70) + print(f'📤 SENDING TO {len(employees)} EMPLOYEE(S)') + print('=' * 70) + + timeout_minutes = 60 if wait_for_signatures else 0 + results = [] + + for i, employee in enumerate(employees, 1): + print(f'[{i}/{len(employees)}]', end=' ') + result = process_employee( + sign_client, + documents, + employee, + output_folder, + timeout_minutes, + wait_for_signatures + ) + results.append(result) + + # Display summary + display_summary(results, wait_for_signatures) + + # Save results to JSON + results_file = output_folder / 'processing-results.json' + results_file.write_text(json.dumps(results, indent=2)) + print(f'📋 Full results saved to: {results_file}') + print() + + except KeyboardInterrupt: + print('\n\n⚠️ Interrupted by user') + sys.exit(1) + except Exception as e: + print(f'\n❌ Error: {e}') + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/test_files/test-sign/README.md b/test_files/test-sign/README.md new file mode 100644 index 0000000..e1b27be --- /dev/null +++ b/test_files/test-sign/README.md @@ -0,0 +1,63 @@ +# Test Files for Employee Policy Distribution + +This folder contains test data for the `send_policies_to_employees.py` script. + +## Contents + +### Policy Documents +- `company-policies.pdf` - Sample company policy document +- `confidentiality-agreement.pdf` - Sample confidentiality agreement + +### Employee Data +- `employees.csv` - List of 3 test employees with name and email + +## Usage + +### From the samples/python directory: + +```bash +# Run the script with these test files +python send_policies_to_employees.py \ + ../../test_files/test-sign \ + ../../test_files/test-sign/employees.csv \ + ./output/test-sign-output + +# Or use separate policies folder and CSV +python send_policies_to_employees.py \ + ../../test_files/test-sign \ + ../../test_files/test-sign/employees.csv \ + ./output/newJoinersJanuary +``` + +### Expected Output Structure + +``` +output/test-sign-output/ +├── john-doe/ +│ ├── signed-policies.pdf +│ ├── envelope-info.json +│ └── envelope-id.txt +├── jane-smith/ +│ ├── signed-policies.pdf +│ ├── envelope-info.json +│ └── envelope-id.txt +└── bob-johnson/ + ├── signed-policies.pdf + ├── envelope-info.json + └── envelope-id.txt +``` + +## Notes + +- **Important**: Update the email addresses in `employees.csv` to real email addresses you can access before testing +- The script will send signature requests to these emails +- You'll need to sign the documents to complete the test +- For testing without waiting, add `--no-wait` flag + +## Test Workflow + +1. Update emails in `employees.csv` to your test email addresses +2. Run the script with the command above +3. Check your email for signature requests +4. Sign the documents through the email link +5. Script will download signed PDFs to output folders diff --git a/test_files/test-sign/company-policies.pdf b/test_files/test-sign/company-policies.pdf new file mode 100755 index 0000000000000000000000000000000000000000..e1d49cd85f148e8bf0a0f0aa16ca1dc9f41c4028 GIT binary patch literal 139766 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 B|LK4x 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 CM=^{5 literal 0 HcmV?d00001 diff --git a/test_files/test-sign/confidentiality-agreement.pdf b/test_files/test-sign/confidentiality-agreement.pdf new file mode 100755 index 0000000000000000000000000000000000000000..46053a8e4ab02b57b70ad11f3fa37748eece4d75 GIT binary patch literal 75673 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 aYW#DL1H`sFr*bk<60!_@d}?rYhW`Q^2DtP9 literal 0 HcmV?d00001 diff --git a/test_files/test-sign/employees.csv b/test_files/test-sign/employees.csv new file mode 100644 index 0000000..721501c --- /dev/null +++ b/test_files/test-sign/employees.csv @@ -0,0 +1,5 @@ +name,email +Isadora Pereira, isamap2410@gmail.com +John Doe,john.doe@company.com +Jane Smith,jane.smith@company.com +Bob Johnson,bob.johnson@company.com diff --git a/test_files/test-sign/sample-company-policies.pdf b/test_files/test-sign/sample-company-policies.pdf new file mode 100755 index 0000000000000000000000000000000000000000..e1d49cd85f148e8bf0a0f0aa16ca1dc9f41c4028 GIT binary patch literal 139766 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 B|LK4x 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 CM=^{5 literal 0 HcmV?d00001 From b454bb17df862f40a9fed9443e35955decbaf9ec Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Fri, 19 Dec 2025 15:35:33 +0000 Subject: [PATCH 09/19] cleaned up onboarding script --- samples/python/employee_policy_onboarding.py | 184 +++++ .../python/helper_functions/sign_helpers.py | 312 +++++++++ samples/python/send_policies_to_employees.py | 655 ------------------ 3 files changed, 496 insertions(+), 655 deletions(-) create mode 100644 samples/python/employee_policy_onboarding.py create mode 100644 samples/python/helper_functions/sign_helpers.py delete mode 100644 samples/python/send_policies_to_employees.py diff --git a/samples/python/employee_policy_onboarding.py b/samples/python/employee_policy_onboarding.py new file mode 100644 index 0000000..bb9fcdc --- /dev/null +++ b/samples/python/employee_policy_onboarding.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +""" +📝 EMPLOYEE POLICY ONBOARDING +============================== + +This script exemplifies a typical HR onboarding workflow for new employees. +As an HR professional, it's necessary to ensure all new hires review and sign +required company policies before their start date. Manual distribution and +tracking of signatures is time-consuming and error-prone, especially when +onboarding multiple employees simultaneously. + +This workflow automates policy distribution and signature collection. The script +processes each new employee individually - for every person in the CSV file, it +creates a signature envelope containing all company policy documents, sends it +via email with signature fields pre-configured, monitors the signing status, and +automatically downloads the signed documents once completed. Each employee's +signed policies are organized in their own folder, creating an audit-ready +archive of onboarding documentation. + + +NOTE: To run this script and see the complete workflow, you must provide a CSV file +with valid employee names and email addresses. The script will send actual signature +requests to these email addresses and wait for them to be signed. + +EMPLOYEE CSV FORMAT: + name,email + John Doe,john.doe@company.com + Jane Smith,jane.smith@company.com + Bob Johnson,bob.johnson@company.com + +USAGE: + python employee_policy_onboarding.py + +EXAMPLE: + python employee_policy_onboarding.py ./policies ./new_hires_jan2025.csv + +OUTPUT STRUCTURE: + output/ + ├── john-doe/ + │ ├── signed-documents/ + │ ├── code-of-conduct-signed.pdf + │ ├── confidentiality-agreement-signed.pdf + │ └── audit-trail.pdf + ├── jane-smith/ + ├── signed-documents/ +""" + +import sys +import csv +import time +import json +import base64 +from pathlib import Path +from api.sign_api import SignAPIClient +from helper_functions.sign_helpers import ( + load_employees_from_csv, + load_policy_documents_from_folder, + create_employee_folder_name, + create_signature_envelope, + add_signature_fields_to_documents, + send_and_monitor_envelope, + download_signed_document, + log_step +) + + + + + + + +def main(): + # Check command-line arguments + if len(sys.argv) != 3: + print('Usage: python send_policies_to_employees.py ') + sys.exit(1) + + # Parse arguments + policies_folder = Path(sys.argv[1]) + employees_csv = Path(sys.argv[2]) + output_folder = Path('output') + + # Validate inputs + if not policies_folder.exists() or not policies_folder.is_dir(): + print(f'❌ Policies folder not found: {policies_folder}') + sys.exit(1) + + if not employees_csv.exists(): + print(f'❌ Employees CSV not found: {employees_csv}') + sys.exit(1) + + # Display header + print('=' * 60) + print('📝 SEND POLICIES TO EMPLOYEES') + print('=' * 60) + print(f'Policies: {policies_folder}') + print(f'Employees: {employees_csv}') + print(f'Output: {output_folder}') + print('=' * 60) + print() + + try: + # Load employees and documents + employees = load_employees_from_csv(employees_csv) + print(f'👥 Found {len(employees)} employee(s)\n') + + documents = load_policy_documents_from_folder(policies_folder) + + # Create output folder + output_folder.mkdir(parents=True, exist_ok=True) + + # Initialize Sign API client + sign_client = SignAPIClient() + + # Process each employee + print('=' * 60) + print(f'📤 PROCESSING {len(employees)} EMPLOYEE(S)') + print('=' * 60) + + success_count = 0 + failed_count = 0 + + for i, employee in enumerate(employees, 1): + name = employee['name'] + email = employee['email'] + + print(f"\n[{i}/{len(employees)}] {name}") + + try: + # Create employee-specific output folder + folder_name = create_employee_folder_name(name) + employee_folder = output_folder / folder_name + employee_folder.mkdir(parents=True, exist_ok=True) + + # Create envelope and upload documents + log_step('📝 Creating envelope...') + envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) + + # Add participant (signer) + log_step('👤 Adding signer...') + participant_data = {'email': email, 'role': 'signer', 'name': name} + participant = sign_client.create_participant(envelope_id, participant_data) + participant_id = participant['ID'] + + # Add signature fields to all documents + log_step('✍️ Adding fields...') + add_signature_fields_to_documents(sign_client, envelope_id, document_ids, participant_id) + + # Send and monitor envelope + log_step('📤 Sending...') + status = send_and_monitor_envelope(sign_client, envelope_id, email, timeout_minutes=60) + + if status != 'sealed': + raise Exception(f'Envelope not signed: {status}') + + # Download signed documents + log_step('📥 Downloading...') + download_signed_document(sign_client, envelope_id, employee_folder, 'signed-policies.zip') + + print(f" ✅ Completed\n") + success_count += 1 + + except Exception as e: + print(f" ❌ FAILED: {e}\n") + failed_count += 1 + + # Display summary + print('=' * 60) + print(f"✅ {success_count} employee(s) completed") + if failed_count > 0: + print(f"❌ {failed_count} failed") + print(f"📂 Output: {output_folder.absolute()}") + print('=' * 60) + + except KeyboardInterrupt: + print('\n\n⚠️ Interrupted by user') + sys.exit(1) + except Exception as e: + print(f'\n❌ Error: {e}') + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py new file mode 100644 index 0000000..3398554 --- /dev/null +++ b/samples/python/helper_functions/sign_helpers.py @@ -0,0 +1,312 @@ +""" +Sign API helper utilities for envelope operations. +""" + +import csv +import json +import time +import zipfile +from pathlib import Path +from datetime import datetime + + +def create_employee_folder_name(employee_name: str) -> str: + """Convert employee name to folder-safe name. + + Args: + employee_name: Full name like "John Doe" + + Returns: + Folder-safe name like "john-doe" + """ + return employee_name.lower().replace(' ', '-').replace('.', '') + + +def load_employees_from_csv(csv_path: Path) -> list[dict]: + """Load employee list from CSV file. + + Args: + csv_path: Path to CSV file with columns: name, email + + Returns: + List of employee dictionaries with 'name' and 'email' + + Raises: + ValueError: If CSV format is invalid + """ + if not csv_path.exists(): + raise ValueError(f'CSV file not found: {csv_path}') + + employees = [] + + with open(csv_path, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + + # Validate CSV has required columns + if 'name' not in reader.fieldnames or 'email' not in reader.fieldnames: + raise ValueError('CSV must have "name" and "email" columns') + + for row in reader: + name = row['name'].strip() + email = row['email'].strip() + + if name and email and '@' in email: + employees.append({'name': name, 'email': email}) + + if not employees: + raise ValueError('No valid employees found in CSV') + + return employees + + +def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: + """Load all PDF files from the policies folder. + + Args: + policies_folder: Path to folder containing policy PDFs + + Returns: + List of document dictionaries with 'name', 'binary', and 'path' + """ + policy_files = list(policies_folder.glob('*.pdf')) + + if not policy_files: + raise ValueError(f'No PDF files found in {policies_folder}') + + print(f'📂 Found {len(policy_files)} policy document(s)\n') + + documents = [] + for pf in policy_files: + with open(pf, 'rb') as f: + binary_data = f.read() + + documents.append({ + 'name': pf.name, + 'binary': binary_data, + 'path': str(pf) + }) + + return documents + + +def create_signature_envelope(sign_client, documents: list[dict], employee_name: str, employee_email: str) -> tuple[str, list[str]]: + """Create envelope and upload documents. + + Args: + sign_client: Sign API client instance + documents: List of document dicts with 'name', 'binary', 'path' + employee_name: Full name of employee + employee_email: Email address of employee + + Returns: + Tuple of (envelope_id, list of document_ids) + """ + # Create empty envelope + envelope_data = { + 'name': f'Company Policies - {employee_name}', + 'mode': "parallel", + 'notification': { + 'subject': f'Please sign: Company Policies', + 'body': f'Hello {employee_name}, please review and sign the attached company policy documents.' + } + } + + envelope = sign_client.create_envelope(envelope_data) + envelope_id = envelope['ID'] + + # Upload documents to envelope + document_ids = [] + + for doc in documents: + doc_name = doc['name'] + doc_binary = doc['binary'] + + # Prepare metadata as JSON string + metadata = json.dumps({'name': doc_name}) + + # Prepare form-data with binary content + files = { + 'metadata': ('metadata', metadata, 'application/json'), + 'payload': (doc_name, doc_binary, 'application/pdf') + } + + headers = {'Authorization': f'Bearer {sign_client._get_token()}'} + + import requests + response = requests.post( + f'{sign_client.base_url}/sign/envelopes/{envelope_id}/documents', + headers=headers, + files=files + ) + + response.raise_for_status() + document = response.json() + + document_id = document['ID'] + document_ids.append(document_id) + + return envelope_id, document_ids + + +def add_signature_fields_to_documents(sign_client, envelope_id: str, document_ids: list[str], participant_id: str) -> None: + """Add signature and date fields to all documents in envelope. + + Args: + sign_client: Sign API client instance + envelope_id: ID of the envelope + document_ids: List of document IDs to add fields to + participant_id: ID of the participant who will sign + """ + for doc_id in document_ids: + # Add signature field + signature_field_data = { + 'participantID': participant_id, + 'type': 'signature', + 'label': 'Your Signature', + 'page': 1, + 'boundingBox': [200, 300, 60, 40], + 'required': True + } + sign_client.create_field(envelope_id, doc_id, signature_field_data) + + # Add date field + date_field_data = { + 'participantID': participant_id, + 'type': 'date', + 'label': 'Date Signed', + 'page': 1, + 'boundingBox': [320, 650, 150, 50], + 'required': True, + 'format': 'MM/DD/YYYY' + } + sign_client.create_field(envelope_id, doc_id, date_field_data) + + +def send_and_monitor_envelope(sign_client, envelope_id: str, email: str, timeout_minutes: int = 60) -> str: + """Send envelope and monitor until signed or timeout. + + Args: + sign_client: Sign API client instance + envelope_id: ID of envelope to send + email: Email address of recipient + timeout_minutes: Maximum time to wait + + Returns: + Final status: 'sealed', 'cancelled', 'timeout', or 'error' + """ + # Send envelope + sign_client.send_for_signing(envelope_id) + + # Log send time and status + send_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f' ✅ Sent at: {send_time}') + print(f' 📧 Email sent to: {email}') + print(f' 🔗 Envelope ID: {envelope_id}') + + # Check initial status + envelope = sign_client.get_envelope(envelope_id) + print(f' 📊 Status: {envelope["status"]}') + + # Monitor for completion + print(f' ⏳ Waiting for signature...') + print(f' ⏱️ Checking every 30 seconds (timeout: {timeout_minutes} minutes)') + + status = monitor_envelope(sign_client, envelope_id, timeout_minutes) + + # Log final status + completion_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f' 📊 Final status: {status}') + print(f' 🕐 Completed at: {completion_time}') + + return status + + +def monitor_envelope(sign_client, envelope_id: str, timeout_minutes: int = 60) -> str: + """Monitor envelope until signed, cancelled, or timeout. + + Args: + sign_client: Sign API client instance + envelope_id: ID of envelope to monitor + timeout_minutes: Maximum time to wait + + Returns: + Final status: 'sealed', 'cancelled', 'timeout', or 'error' + """ + check_interval = 30 # seconds + max_checks = (timeout_minutes * 60) // check_interval + + for i in range(max_checks): + try: + envelope = sign_client.get_envelope(envelope_id) + status = envelope['status'] + + if status == 'sealed': + return 'sealed' + elif status in ['cancelled', 'rejected', 'deleted']: + return 'cancelled' + + if i < max_checks - 1: + time.sleep(check_interval) + + except Exception as e: + print(f' ⚠️ Error checking status: {e}') + return 'error' + + return 'timeout' + + +def download_signed_document(sign_client, envelope_id: str, output_folder: Path, document_name: str) -> Path: + """Download sealed envelope and extract signed documents. + + The API returns a ZIP file containing: + - All signed PDF documents + - Audit trail document + + This function extracts the contents and removes the ZIP file. + + Args: + sign_client: Sign API client instance + envelope_id: ID of sealed envelope + output_folder: Employee-specific output folder + document_name: Name for the saved file (should end with .zip) + + Returns: + Path to extracted documents folder + """ + # Download sealed envelope (returns ZIP file) + zip_bytes = sign_client.download_sealed_envelope(envelope_id) + + # Save as temporary ZIP file + if not document_name.endswith('.zip'): + document_name = document_name.replace('.pdf', '.zip') + + temp_zip_path = output_folder / document_name + temp_zip_path.write_bytes(zip_bytes) + + # Extract the ZIP contents + extract_folder = output_folder / 'signed-documents' + extract_folder.mkdir(exist_ok=True) + + with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref: + zip_ref.extractall(extract_folder) + + # Delete the ZIP file after extraction + temp_zip_path.unlink() + + # Save envelope metadata + envelope = sign_client.get_envelope(envelope_id) + json_path = output_folder / 'envelope-info.json' + json_path.write_text(json.dumps(envelope, indent=2)) + + print(f' 💾 Extracted to: {extract_folder}') + + return extract_folder + + +def log_step(message: str) -> None: + """Log a processing step with consistent formatting. + + Args: + message: The message to log + """ + print(f' {message}') diff --git a/samples/python/send_policies_to_employees.py b/samples/python/send_policies_to_employees.py deleted file mode 100644 index 219b437..0000000 --- a/samples/python/send_policies_to_employees.py +++ /dev/null @@ -1,655 +0,0 @@ -#!/usr/bin/env python3 -""" -📝 SEND POLICY DOCUMENTS TO MULTIPLE EMPLOYEES -=============================================== - -This script sends multiple policy documents from a folder to multiple employees for their -signature and saves each signed copy in a dedicated folder per employee. - -WORKFLOW: - 1. Load all policy documents from a folder (PDF, Word documents) - 2. Convert non-PDF documents to PDF automatically - 3. Read employee list from CSV file (name, email) - 4. For each employee: - - Create signature envelope with ALL policy documents - - Send for electronic signature - - Monitor until signed - - Save signed documents as ZIP (contains all signed PDFs + audit trail) - -EMPLOYEE CSV FORMAT: - name,email - John Doe,john.doe@company.com - Jane Smith,jane.smith@company.com - Bob Johnson,bob.johnson@company.com - -USAGE: - python send_policies_to_employees.py - -EXAMPLE: - python send_policies_to_employees.py ./policies ./employees.csv ./newJoinersJanuary - -OUTPUT STRUCTURE: - newJoinersJanuary/ - ├── john-doe/ - │ ├── signed-policies.zip # ZIP file with all signed documents - │ ├── signed-documents/ # Extracted contents: - │ │ ├── policy1-signed.pdf # - Signed policy documents - │ │ ├── policy2-signed.pdf - │ │ └── audit-trail.pdf # - Audit trail document - │ ├── envelope-info.json - │ └── envelope-id.txt - ├── jane-smith/ - │ ├── signed-policies.zip - │ ├── signed-documents/ - │ ├── envelope-info.json - │ └── envelope-id.txt - └── bob-johnson/ - ├── signed-policies.zip - ├── signed-documents/ - ├── envelope-info.json - └── envelope-id.txt - -NOTE: The Nitro Sign API returns signed envelopes as ZIP files containing all signed PDFs - plus an audit trail. The script automatically extracts the ZIP for convenience. -""" - -import sys -import csv -import time -import json -import base64 -from pathlib import Path -from api.sign_api import SignAPIClient - - -# def load_employees_from_csv(csv_path: Path) -> list[dict]: -# """Load employee list from CSV file. - -# Args: -# csv_path: Path to CSV file with columns: name, email - -# Returns: -# List of employee dictionaries with 'name' and 'email' -# """ -# employees = [] - -# with open(csv_path, 'r', encoding='utf-8') as f: -# reader = csv.DictReader(f) - -# # Validate CSV has required columns -# if 'name' not in reader.fieldnames or 'email' not in reader.fieldnames: -# raise ValueError('CSV must have "name" and "email" columns') - -# for row in reader: -# name = row['name'].strip() -# email = row['email'].strip() - -# if name and email and '@' in email: -# employees.append({'name': name, 'email': email}) -# else: -# print(f' ⚠️ Skipping invalid row: {row}') - -# return employees - - -def create_employee_folder_name(employee_name: str) -> str: - """Convert employee name to folder-safe name. - - Args: - employee_name: Full name like "John Doe" - - Returns: - Folder-safe name like "john-doe" - """ - return employee_name.lower().replace(' ', '-').replace('.', '') - -def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: - - print(f'📂 Loading policy documents from: {policies_folder}') - - # Find only PDF files (no conversion for now, keep it simple) - policy_files = list(policies_folder.glob('*.pdf')) - - if not policy_files: - raise ValueError(f'No PDF files found in {policies_folder}') - - print(f' ✅ Found {len(policy_files)} PDF document(s)') - - documents = [] - for pf in policy_files: - print(f' • {pf.name}') - - # Read binary content - with open(pf, 'rb') as f: - binary_data = f.read() - - documents.append({ - 'name': pf.name, - 'binary': binary_data, - 'path': str(pf) # Convert PosixPath to string - }) - - print() - return documents - - -def create_signature_envelope( - sign_client: SignAPIClient, - documents: list[Path], # List of file paths - employee_name: str, - employee_email: str -) -> tuple[str, list[str]]: # Returns envelope_id and list of document_ids - """Create envelope and upload documents. - - Args: - sign_client: Sign API client instance - documents: List of Path objects to PDF files - employee_name: Full name of employee - employee_email: Email address of employee - - Returns: - Tuple of (envelope_id, list of document_ids) - """ - - # ============================================================ - # STEP 1: Create empty envelope - # ============================================================ - print(f' 📝 Step 1: Creating empty envelope...') - - envelope_data = { - 'name': f'Company Policies - {employee_name}', - 'mode': "parallel", - 'notification': { - 'subject': f'Please sign: Company Policies', - 'body': f'Hello {employee_name}, please review and sign the attached company policy documents.' - } - } - print(f' 🔍 DEBUG - Sending: {envelope_data}') - - envelope = sign_client.create_envelope(envelope_data) - envelope_id = envelope['ID'] - print(f' ✅ Envelope created: {envelope_id}') - - # ============================================================ - # STEP 2: Upload documents to envelope - # ============================================================ - print(f'\n 📄 Step 2: Uploading {len(documents)} document(s)...') - document_ids = [] - - for i, doc in enumerate(documents, 1): - doc_name = doc['name'] - doc_binary = doc['binary'] - doc_path = doc['path'] - - print(f' [{i}/{len(documents)}] Uploading: {doc_name}') - - # Prepare metadata as JSON string - import json - metadata = json.dumps({'name': doc_name}) - - # Prepare form-data with binary content - files = { - 'metadata': ('metadata', metadata, 'application/json'), - 'payload': (doc_name, doc_binary, 'application/pdf') - } - - headers = {'Authorization': f'Bearer {sign_client._get_token()}'} - - import requests - response = requests.post( - f'{sign_client.base_url}/sign/envelopes/{envelope_id}/documents', - headers=headers, - files=files - ) - - response.raise_for_status() - document = response.json() - - document_id = document['ID'] - document_ids.append(document_id) - print(f' ✅ Uploaded: {document_id}') - - return envelope_id, document_ids - - - -def monitor_envelope(sign_client: SignAPIClient, envelope_id: str, timeout_minutes: int = 60) -> str: - """Monitor envelope until signed, cancelled, or timeout. - - Args: - sign_client: Sign API client instance - envelope_id: ID of envelope to monitor - timeout_minutes: Maximum time to wait - - Returns: - Final status: 'sealed', 'cancelled', 'timeout', or 'error' - """ - check_interval = 30 # seconds - max_checks = (timeout_minutes * 60) // check_interval - - for i in range(max_checks): - try: - envelope = sign_client.get_envelope(envelope_id) - status = envelope['status'] - - if status == 'sealed': - return 'sealed' - elif status in ['cancelled', 'rejected', 'deleted']: - return 'cancelled' - - if i < max_checks - 1: - time.sleep(check_interval) - - except Exception as e: - print(f' ⚠️ Error checking status: {e}') - return 'error' - - return 'timeout' - - -def download_signed_document( - sign_client: SignAPIClient, - envelope_id: str, - output_folder: Path, - document_name: str -) -> Path: - """Download sealed envelope to employee folder. - - The API returns a ZIP file containing: - - All signed PDF documents - - Audit trail document - - Args: - sign_client: Sign API client instance - envelope_id: ID of sealed envelope - output_folder: Employee-specific output folder - document_name: Name for the saved file (should end with .zip) - - Returns: - Path to saved ZIP file - """ - # Download sealed envelope (returns ZIP file) - zip_bytes = sign_client.download_sealed_envelope(envelope_id) - - # Save as ZIP file - if not document_name.endswith('.zip'): - document_name = document_name.replace('.pdf', '.zip') - - output_path = output_folder / document_name - output_path.write_bytes(zip_bytes) - - # Also extract the ZIP contents for convenience - import zipfile - extract_folder = output_folder / 'signed-documents' - extract_folder.mkdir(exist_ok=True) - - with zipfile.ZipFile(output_path, 'r') as zip_ref: - zip_ref.extractall(extract_folder) - - print(f' 📦 ZIP saved to: {output_path}') - print(f' 📂 Extracted to: {extract_folder}') - - # Save envelope metadata - envelope = sign_client.get_envelope(envelope_id) - json_path = output_folder / 'envelope-info.json' - json_path.write_text(json.dumps(envelope, indent=2)) - - return output_path - - -def process_employee( - sign_client: SignAPIClient, - documents: list[str], - employee: dict, - base_output_folder: Path, - timeout_minutes: int, - wait_for_signatures: bool -) -> dict: - """Process signature request for one employee with multiple documents. - - Args: - sign_client: Sign API client instance - documents: List of policy document dictionaries - employee: Employee dict with 'name' and 'email' - base_output_folder: Base folder for all signed documents - timeout_minutes: How long to wait for signature - wait_for_signatures: If True, wait for signature; if False, just send - - Returns: - Result dictionary with status and details - """ - name = employee['name'] - email = employee['email'] - - print(f'\n📧 Processing: {name} ({email})') - print(f' {"─" * 60}') - - try: - # Create employee-specific output folder - folder_name = create_employee_folder_name(name) - employee_folder = base_output_folder / folder_name - employee_folder.mkdir(parents=True, exist_ok=True) - - # Documents are already loaded and passed as parameter - # STEP 1 & 2: Create envelope and upload documents - print(f' 📝 Creating envelope for {name}...') - envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) - print(f' ✅ Envelope created: {envelope_id}') - print(f' ✅ Documents uploaded: {len(document_ids)}') - for i, doc_id in enumerate(document_ids, 1): - print(f' [{i}] {doc_id}') - - # ============================================================ - # STEP 3: Add participant (signer) - # ============================================================ - print(f'\n 👤 Step 3: Adding participant...') - - participant_data = { - 'email': email, - 'role': 'signer', - 'name': name - } - - print(f' 🔍 DEBUG - Participant data: {participant_data}') - - participant = sign_client.create_participant(envelope_id, participant_data) - participant_id = participant['ID'] - print(f' ✅ Participant added: {participant_id}') - - # ============================================================ - # STEP 4: Add signature fields to each document - # ============================================================ - print(f'\n ✍️ Step 4: Adding signature fields...') - - for i, doc_id in enumerate(document_ids, 1): - print(f' [{i}/{len(document_ids)}] Adding fields for document {doc_id}') - - # Add signature field - signature_field_data = { - 'participantID': participant_id, - 'type': 'signature', - 'label': 'Your Signature', - 'page': 1, - 'boundingBox': [200, 300, 60, 40], # [x, y, width, height] - 'required': True - } - - signature_field = sign_client.create_field(envelope_id, doc_id, signature_field_data) - print(f' ✅ Signature field: {signature_field["ID"]}') - - # Add date field - date_field_data = { - 'participantID': participant_id, - 'type': 'date', - 'label': 'Date Signed', - 'page': 1, - 'boundingBox': [320, 650, 150, 50], # [x, y, width, height] - 'required': True, - 'format': 'MM/DD/YYYY' - } - - date_field = sign_client.create_field(envelope_id, doc_id, date_field_data) - print(f' ✅ Date field: {date_field["ID"]}') - - - envelope = sign_client.get_envelope(envelope_id) - print(f"!!!!!!!!!!!!!!!!!Envelope status: {envelope['status']}") - # ============================================================ - # STEP 5: Send envelope for signing - # ============================================================ - print(f'\n 📤 Step 5: Sending envelope for signing...') - sign_client.send_for_signing(envelope_id) - print(f' ✅ Envelope sent to {email}') - envelope = sign_client.get_envelope(envelope_id) - - print(f"!!!!!!!!!!!!!!!!!Envelope status: {envelope['status']}") - - # ============================================================ - # STEP 6: Monitor status and download signed document - # ============================================================ - if wait_for_signatures: - print(f'\n ⏳ Step 6: Monitoring signing status...') - print(f' 💡 Employee will receive email at {email}') - print(f' ⏱️ Checking every 30 seconds (timeout: {timeout_minutes} min)...') - - status = monitor_envelope(sign_client, envelope_id, timeout_minutes) - - if status == 'sealed': - print(f' ✅ Document signed!') - print(f' 📥 Downloading signed document...') - - output_path = download_signed_document( - sign_client, - envelope_id, - employee_folder, - 'signed-policies.pdf' - ) - - print(f' 💾 Saved to: {output_path}') - - return { - 'status': 'success', - 'name': name, - 'email': email, - 'envelope_id': envelope_id, - 'output_path': str(output_path), - 'num_documents': len(document_ids) - } - elif status == 'timeout': - print(f' ⏱️ Timeout - not signed yet') - return { - 'status': 'timeout', - 'name': name, - 'email': email, - 'envelope_id': envelope_id, - 'folder': str(employee_folder), - 'num_documents': len(document_ids) - } - elif status == 'cancelled': - print(f' ❌ Envelope cancelled or rejected') - return { - 'status': 'cancelled', - 'name': name, - 'email': email, - 'envelope_id': envelope_id - } - else: - print(f' ❌ Error monitoring envelope') - return { - 'status': 'error', - 'name': name, - 'email': email, - 'envelope_id': envelope_id - } - else: - # Not waiting for signatures - print(f' ✅ Envelope sent (not waiting for signature)') - return { - 'status': 'sent', - 'name': name, - 'email': email, - 'envelope_id': envelope_id, - 'folder': str(employee_folder), - 'num_documents': len(documents) - } - - - - except Exception as e: - print(f' ❌ Error: {e}') - return { - 'status': 'failed', - 'name': name, - 'email': email, - 'error': str(e) - } - - -def display_summary(results: list[dict], wait_for_signatures: bool): - """Display final summary of all operations. - - Args: - results: List of result dictionaries - wait_for_signatures: Whether signatures were waited for - """ - print('\n' + '=' * 70) - print('📊 SUMMARY') - print('=' * 70) - - success = [r for r in results if r['status'] == 'success'] - created = [r for r in results if r['status'] == 'created'] # NEW: for Step 1 testing - sent = [r for r in results if r['status'] == 'sent'] - timeout = [r for r in results if r['status'] == 'timeout'] - cancelled = [r for r in results if r['status'] == 'cancelled'] - failed = [r for r in results if r['status'] == 'failed'] - - # TESTING MODE: Show created envelopes - print(f'✅ Envelopes created (Step 1): {len(created)}') - print(f'✅ Completed and signed: {len(success)}') - print(f'📤 Sent (not waiting): {len(sent)}') - print(f'⏱️ Timeout: {len(timeout)}') - print(f'❌ Cancelled/Rejected: {len(cancelled)}') - print(f'❌ Failed: {len(failed)}') - - print() - - # Show created envelopes (Step 1 testing) - if created: - print('✅ Envelopes created (testing Step 1):') - for r in created: - num_docs = r.get('num_documents', 0) - print(f" • {r['name']}: envelope {r['envelope_id']} ({num_docs} documents pending)") - print(f" Folder: {r['folder']}") - print() - - # Show successful completions - if success: - print('✅ Signed documents saved to:') - for r in success: - num_docs = r.get('num_documents', 0) - print(f" • {r['name']}: {r['output_path']} ({num_docs} documents)") - print() - - # Show timeouts - if timeout: - print('⏱️ Not signed yet (check these later):') - for r in timeout: - print(f" • {r['name']}: envelope {r['envelope_id']}") - print(f" Command: python download_envelope.py {r['envelope_id']} {r['folder']}/signed-policies.pdf") - print() - - # Show sent (not waiting) - if sent: - print('📤 Envelopes sent (check status later):') - for r in sent: - num_docs = r.get('num_documents', 0) - print(f" • {r['name']}: envelope {r['envelope_id']} ({num_docs} documents)") - print(f" Folder: {r['folder']}") - print() - - # Show failures - if failed: - print('❌ Failed to process:') - for r in failed: - print(f" • {r['name']} ({r['email']}): {r.get('error', 'Unknown error')}") - print() - - print('=' * 70) - - -def main(): - # Check command-line arguments - if len(sys.argv) < 4: - print('Usage: python send_policies_to_employees.py [--no-wait]') - sys.exit(1) - - # Parse arguments - policies_folder = Path(sys.argv[1]) - employees_csv = Path(sys.argv[2]) - output_folder = Path(sys.argv[3]) - wait_for_signatures = '--no-wait' not in sys.argv - - # Validate inputs - if not policies_folder.exists() or not policies_folder.is_dir(): - print(f'❌ Policies folder not found: {policies_folder}') - sys.exit(1) - - if not employees_csv.exists(): - print(f'❌ Employees CSV not found: {employees_csv}') - sys.exit(1) - - # Display header - print('=' * 70) - print('📝 SEND POLICY DOCUMENTS TO MULTIPLE EMPLOYEES') - print('=' * 70) - print(f'Policies folder: {policies_folder}') - print(f'Employees list: {employees_csv}') - print(f'Output folder: {output_folder}') - print(f'Wait for signatures: {"Yes" if wait_for_signatures else "No (send only)"}') - print('=' * 70) - print() - - try: - # Load employees - print('👥 Loading employees from CSV...') - employees = [{'name': "Isadora", 'email': "isamap2410@gmail.com"}, ] - # {'name': "John Doe", 'email': "john.doe@example.com"} - - - # load_employees_from_csv(employees_csv) - print(f' ✅ Found {len(employees)} employee(s)') - - if len(employees) == 0: - print('❌ No valid employees found in CSV') - sys.exit(1) - - # Load and prepare all policy documents - print() - documents = load_policy_documents_from_folder(policies_folder) - - # Create output folder - output_folder.mkdir(parents=True, exist_ok=True) - - # Initialize Sign API client - sign_client = SignAPIClient() - - # Process each employee - print() - print('=' * 70) - print(f'📤 SENDING TO {len(employees)} EMPLOYEE(S)') - print('=' * 70) - - timeout_minutes = 60 if wait_for_signatures else 0 - results = [] - - for i, employee in enumerate(employees, 1): - print(f'[{i}/{len(employees)}]', end=' ') - result = process_employee( - sign_client, - documents, - employee, - output_folder, - timeout_minutes, - wait_for_signatures - ) - results.append(result) - - # Display summary - display_summary(results, wait_for_signatures) - - # Save results to JSON - results_file = output_folder / 'processing-results.json' - results_file.write_text(json.dumps(results, indent=2)) - print(f'📋 Full results saved to: {results_file}') - print() - - except KeyboardInterrupt: - print('\n\n⚠️ Interrupted by user') - sys.exit(1) - except Exception as e: - print(f'\n❌ Error: {e}') - sys.exit(1) - - -if __name__ == '__main__': - main() From 5fc597511e88307456a8011e9fd2a10015453e8f Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Mon, 22 Dec 2025 00:07:44 +0000 Subject: [PATCH 10/19] fixes --- samples/python/.gitignore | 7 ++--- samples/python/Taskfile.yml | 28 ++++++++++---------- samples/python/employee_policy_onboarding.py | 8 +----- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/samples/python/.gitignore b/samples/python/.gitignore index 82f2c3e..fd7ba57 100644 --- a/samples/python/.gitignore +++ b/samples/python/.gitignore @@ -14,9 +14,6 @@ __pycache__/ .Python # Virtual environments (never commit these!) -venv/ -env/ -.venv/ -.env/ + platform-integrations/ -nitro-venv/ + diff --git a/samples/python/Taskfile.yml b/samples/python/Taskfile.yml index a303a09..01ce63d 100644 --- a/samples/python/Taskfile.yml +++ b/samples/python/Taskfile.yml @@ -2,11 +2,11 @@ version: '3' tasks: smart-redact: - desc: "Smart redact PII from a document (e.g., task smart-redact INPUT=resume.pdf OUTPUT=redacted.pdf)" + desc: "Smart redact PII from documents in a folder (e.g., task smart-redact INPUT_DIR=./documents OUTPUT_DIR=./redacted)" cmds: - - python smart_redact_pii.py {{.INPUT}} {{.OUTPUT}} + - python smart_redact_pii.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" requires: - vars: [INPUT, OUTPUT] + vars: [INPUT_DIR, OUTPUT_DIR] convert: desc: "Convert document from CLI (e.g., task convert INPUT=doc.docx OUTPUT=doc.pdf FORMAT=pdf)" @@ -37,7 +37,7 @@ tasks: vars: [INPUT_DIR, OUTPUT_DIR, PASSWORD] redact-keyword: - desc: "Redact by keyword search (e.g., task redact-keyword INPUT=doc.pdf OUTPUT=redacted.pdf KEYWORDS='confidential secret')" + desc: "Redact specific keywords from a PDF (e.g., task redact-keyword INPUT=doc.pdf OUTPUT=redacted.pdf KEYWORDS='confidential secret')" cmds: - python redact_by_keyword.py {{.INPUT}} {{.OUTPUT}} {{.KEYWORDS}} requires: @@ -54,18 +54,18 @@ tasks: - cp .env.example .env - echo "✅ Created .env file - please update with your credentials" - # ========== Sign API Tasks (eSignature) ========== - - send-policies: - desc: "Send policy PDF to multiple employees for signature (e.g., task send-policies PDF=policies.pdf CSV=employees.csv OUTPUT=./signed)" + prepare-distribution: + desc: "Prepare documents for external distribution (convert, compress, remove metadata) (e.g., task prepare-distribution INPUT_DIR=./brochures OUTPUT_DIR=./ready)" cmds: - - python send_policies_to_employees.py {{.PDF}} {{.CSV}} {{.OUTPUT}} + - python prepare_pdf_for_distribution.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" requires: - vars: [PDF, CSV, OUTPUT] + vars: [INPUT_DIR, OUTPUT_DIR] + + # ========== Sign API Tasks (eSignature) ========== - send-policies-no-wait: - desc: "Send policy PDF to all employees without waiting (e.g., task send-policies-no-wait PDF=policies.pdf CSV=employees.csv OUTPUT=./signed)" + onboard-employees: + desc: "Send company policies to new employees for signature (e.g., task onboard-employees POLICIES_DIR=./policies CSV=new_hires.csv)" cmds: - - python send_policies_to_employees.py {{.PDF}} {{.CSV}} {{.OUTPUT}} --no-wait + - python employee_policy_onboarding.py "{{.POLICIES_DIR}}" "{{.CSV}}" requires: - vars: [PDF, CSV, OUTPUT] + vars: [POLICIES_DIR, CSV] diff --git a/samples/python/employee_policy_onboarding.py b/samples/python/employee_policy_onboarding.py index bb9fcdc..ad7066e 100644 --- a/samples/python/employee_policy_onboarding.py +++ b/samples/python/employee_policy_onboarding.py @@ -63,16 +63,10 @@ log_step ) - - - - - - def main(): # Check command-line arguments if len(sys.argv) != 3: - print('Usage: python send_policies_to_employees.py ') + print('Usage: python employee_policy_onboarding.py ') sys.exit(1) # Parse arguments From 84d8dc86c91040ed7313be584d2b9c0be8256a8a Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 23 Dec 2025 15:13:52 +0000 Subject: [PATCH 11/19] updated code to use uv, fix ruff errors --- samples/python/.gitignore | 7 +- samples/python/api/platform_api.py | 140 ++++---- samples/python/api/sign_api.py | 253 +++++++-------- samples/python/batch_process.py | 48 +-- samples/python/bulk_password_protect.py | 47 +-- samples/python/convert_cli.py | 42 +-- samples/python/employee_policy_onboarding.py | 174 +++++----- samples/python/extract_data.py | 61 ++-- .../helper_functions/document_helpers.py | 24 +- .../python/helper_functions/sign_helpers.py | 302 ++++++++++-------- .../python/prepare_pdf_for_distribution.py | 58 ++-- samples/python/pyproject.toml | 111 +++++++ samples/python/quickstart.py | 40 +-- samples/python/redact_by_keyword.py | 77 ++--- samples/python/requirements.txt | 3 - samples/python/smart_redact_pii.py | 72 ++--- samples/python/uv.lock | 289 +++++++++++++++++ 17 files changed, 1080 insertions(+), 668 deletions(-) create mode 100644 samples/python/pyproject.toml delete mode 100644 samples/python/requirements.txt create mode 100644 samples/python/uv.lock diff --git a/samples/python/.gitignore b/samples/python/.gitignore index fd7ba57..56e8318 100644 --- a/samples/python/.gitignore +++ b/samples/python/.gitignore @@ -2,7 +2,6 @@ .env # Temporary test files and outputs -temp/ output/ test_output/ @@ -12,8 +11,8 @@ __pycache__/ *.pyo *.pyd .Python +*.egg-info/ -# Virtual environments (never commit these!) - -platform-integrations/ +# Virtual environment +.venv/ diff --git a/samples/python/api/platform_api.py b/samples/python/api/platform_api.py index 969e253..0cd562d 100644 --- a/samples/python/api/platform_api.py +++ b/samples/python/api/platform_api.py @@ -1,167 +1,165 @@ #!/usr/bin/env python """Platform API client for Nitro Platform integrations.""" -import os -import time import json import mimetypes -import requests +import os +import time from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal + +import requests from dotenv import load_dotenv +if TYPE_CHECKING: + from pathlib import Path + load_dotenv() @dataclass class PlatformAPIClient: """Synchronous client for Nitro Platform API operations.""" - - base_url: str = field(default_factory=lambda: os.getenv('PLATFORM_BASE_URL', 'https://api.gonitro.dev')) - client_id: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_ID')) - client_secret: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_SECRET')) + + base_url: str = field( + default_factory=lambda: os.getenv("PLATFORM_BASE_URL", "https://api.gonitro.dev") + ) + client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID")) + client_secret: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET")) _token: str | None = field(default=None, init=False) _token_expiry: float = field(default=0, init=False) - + def _get_token(self) -> str: """Get or refresh OAuth2 access token.""" if self._token and time.time() < self._token_expiry: return self._token - + response = requests.post( f"{self.base_url}/oauth/token", - json={"clientID": self.client_id, "clientSecret": self.client_secret} + json={"clientID": self.client_id, "clientSecret": self.client_secret}, ) response.raise_for_status() data = response.json() - self._token = data['accessToken'] - self._token_expiry = time.time() + data.get('expiresIn', 3600) - 60 + self._token = data["accessToken"] + self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 return self._token - + def _request( self, endpoint: Literal["conversions", "extractions", "transformations"], method: str, file_path: Path, - params: dict[str, Any] = None + params: dict[str, Any] | None = None, ) -> dict[str, Any]: """Make API request with file upload.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - + # Detect MIME type or use octet-stream as fallback - mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' - + mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" + # Read file into memory and include content-type - files = {'file': (file_path.name, file_path.read_bytes(), mime_type)} - data = { - 'method': method, - 'params': json.dumps(params or {}) - } - + files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} + data = {"method": method, "params": json.dumps(params or {})} + response = requests.post( - f"{self.base_url}/{endpoint}", - headers=headers, - files=files, - data=data + f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data ) - + response.raise_for_status() return response.json() - + def _request_bytes( self, endpoint: Literal["conversions", "extractions", "transformations"], method: str, file_path: Path, - params: dict[str, Any] = None + params: dict[str, Any] | None = None, ) -> bytes: """Make API request and return raw bytes.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - + # Detect MIME type or use octet-stream as fallback - mime_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream' - + mime_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream" + # Read file into memory and include content-type - files = {'file': (file_path.name, file_path.read_bytes(), mime_type)} - data = { - 'method': method, - 'params': json.dumps(params or {}) - } - + files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} + data = {"method": method, "params": json.dumps(params or {})} + response = requests.post( - f"{self.base_url}/{endpoint}", - headers=headers, - files=files, - data=data + f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data ) - + response.raise_for_status() result = response.json() - + # Download from S3 URL - download_url = result['result']['file']['URL'] + download_url = result["result"]["file"]["URL"] download_response = requests.get(download_url) download_response.raise_for_status() return download_response.content - + def convert(self, file_path: Path, to_format: str) -> bytes: """Convert document to specified format.""" return self._request_bytes("conversions", "convert", file_path, {"to": to_format}) - + def extract_text(self, file_path: Path) -> dict[str, Any]: """Extract text from document.""" return self._request("extractions", "extract-text", file_path) - + def extract_forms(self, file_path: Path) -> dict[str, Any]: """Extract form data from PDF.""" return self._request("extractions", "extract-forms", file_path) - + def extract_tables(self, file_path: Path) -> dict[str, Any]: """Extract table data from PDF.""" return self._request("extractions", "extract-tables", file_path) - + def detect_pii(self, file_path: Path, language: str = "en") -> dict[str, Any]: """Detect PII and return bounding boxes.""" - return self._request("extractions", "extract-pii-bounding-boxes", file_path, {"language": language}) - + return self._request( + "extractions", "extract-pii-bounding-boxes", file_path, {"language": language} + ) + def find_text_boxes(self, file_path: Path, texts: list[str]) -> dict[str, Any]: """Find bounding boxes for specified text strings.""" - return self._request("extractions", "extract-text-bounding-boxes", file_path, {"texts": texts}) - + return self._request( + "extractions", "extract-text-bounding-boxes", file_path, {"texts": texts} + ) + def redact(self, file_path: Path, redactions: list[dict]) -> bytes: """Redact specified bounding boxes.""" - return self._request_bytes("transformations", "redact", file_path, {"redactions": redactions}) - + return self._request_bytes( + "transformations", "redact", file_path, {"redactions": redactions} + ) + def password_protect(self, file_path: Path, password: str) -> bytes: """Add password protection to PDF.""" return self._request_bytes( "transformations", "protect", file_path, - {"ownerPassword": password, "userPassword": password} + {"ownerPassword": password, "userPassword": password}, ) - + def compress(self, file_path: Path, level: int = 2) -> bytes: """Compress PDF (level 1-3).""" return self._request_bytes("transformations", "compress", file_path, {"level": level}) - + def merge(self, file_paths: list[Path]) -> bytes: """Merge multiple PDFs.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - - files = [('file', open(fp, 'rb')) for fp in file_paths] - data = {'method': 'merge', 'params': '{}'} - + + # Open files with context manager + opened_files = [fp.open("rb") for fp in file_paths] try: + files = [("file", f) for f in opened_files] + data = {"method": "merge", "params": "{}"} + response = requests.post( - f"{self.base_url}/transformations", - headers=headers, - files=files, - data=data + f"{self.base_url}/transformations", headers=headers, files=files, data=data ) response.raise_for_status() return response.content finally: - for _, f in files: + for f in opened_files: f.close() diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py index 1418e32..5f7fa9c 100644 --- a/samples/python/api/sign_api.py +++ b/samples/python/api/sign_api.py @@ -1,316 +1,297 @@ #!/usr/bin/env python """Sign API client for Nitro Sign integrations (eSignature operations).""" +import json import os import time -import json -import requests from dataclasses import dataclass, field -from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any + +import requests from dotenv import load_dotenv +if TYPE_CHECKING: + from pathlib import Path + load_dotenv() @dataclass class SignAPIClient: """Synchronous client for Nitro Sign API operations (eSignature/envelopes).""" - - base_url: str = field(default_factory=lambda: os.getenv('PLATFORM_BASE_URL', 'https://api.gonitro.dev')) - client_id: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_ID')) - client_secret: str = field(default_factory=lambda: os.getenv('PLATFORM_CLIENT_SECRET')) + + base_url: str = field( + default_factory=lambda: os.getenv("PLATFORM_BASE_URL", "https://api.gonitro.dev") + ) + client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID")) + client_secret: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET")) _token: str | None = field(default=None, init=False) _token_expiry: float = field(default=0, init=False) - + def _get_token(self) -> str: """Get or refresh OAuth2 access token.""" if self._token and time.time() < self._token_expiry: return self._token - + response = requests.post( - f'{self.base_url}/oauth/token', - json={'clientID': self.client_id, 'clientSecret': self.client_secret} + f"{self.base_url}/oauth/token", + json={"clientID": self.client_id, "clientSecret": self.client_secret}, ) response.raise_for_status() data = response.json() - self._token = data['accessToken'] - self._token_expiry = time.time() + data.get('expiresIn', 3600) - 60 + self._token = data["accessToken"] + self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 return self._token - + def _request( self, method: str, endpoint: str, - json_data: dict[str, Any] = None, - params: dict[str, Any] = None + json_data: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, ) -> dict[str, Any]: """Make authenticated API request returning JSON.""" - headers = {'Authorization': f'Bearer {self._get_token()}'} - + headers = {"Authorization": f"Bearer {self._get_token()}"} + response = requests.request( method=method, - url=f'{self.base_url}{endpoint}', + url=f"{self.base_url}{endpoint}", headers=headers, json=json_data, - params=params + params=params, ) - + try: response.raise_for_status() - except requests.exceptions.HTTPError as e: + except requests.exceptions.HTTPError: # Try to get error details from response try: error_detail = response.json() - print(f' ❌ API Error Response: {error_detail}') - except: - print(f' ❌ API Error (no JSON): {response.text}') - raise e - + print(f" ❌ API Error Response: {error_detail}") + except Exception: # noqa: BLE001 + print(f" ❌ API Error (no JSON): {response.text}") + raise + return response.json() - + def _request_bytes( - self, - method: str, - endpoint: str, - params: dict[str, Any] = None + self, method: str, endpoint: str, params: dict[str, Any] | None = None ) -> bytes: """Make authenticated API request returning binary data.""" - headers = {'Authorization': f'Bearer {self._get_token()}'} - + headers = {"Authorization": f"Bearer {self._get_token()}"} + response = requests.request( - method=method, - url=f'{self.base_url}{endpoint}', - headers=headers, - params=params + method=method, url=f"{self.base_url}{endpoint}", headers=headers, params=params ) - + response.raise_for_status() return response.content - + # ========== Envelope Management ========== - + def list_envelopes( - self, - page_after: str = None, - page_before: str = None + self, page_after: str | None = None, page_before: str | None = None ) -> dict[str, Any]: """List all envelopes with cursor-based pagination. - + Args: page_after: Cursor token to get items after the last item from previous response page_before: Cursor token to get items before the last item from previous response - + Returns: Dict with 'items' (list of envelopes) and optional 'nextPage' (cursor token) """ params = {} if page_after: - params['pageAfter'] = page_after + params["pageAfter"] = page_after elif page_before: - params['pageBefore'] = page_before - - return self._request('GET', '/sign/envelopes', params=params) - + params["pageBefore"] = page_before + + return self._request("GET", "/sign/envelopes", params=params) + def create_envelope(self, envelope_data: dict[str, Any]) -> dict[str, Any]: """Create a new envelope. - + Args: envelope_data: Envelope configuration including name, documents, participants, fields - + Returns: Created envelope with ID and status """ - return self._request('POST', '/sign/envelopes', json_data=envelope_data) - + return self._request("POST", "/sign/envelopes", json_data=envelope_data) + def get_envelope(self, envelope_id: str) -> dict[str, Any]: """Get envelope details by ID. - + Args: envelope_id: UUID of the envelope - + Returns: Envelope details including status, participants, documents """ - return self._request('GET', f'/sign/envelopes/{envelope_id}') - + return self._request("GET", f"/sign/envelopes/{envelope_id}") + def update_envelope(self, envelope_id: str, updates: dict[str, Any]) -> dict[str, Any]: """Update an envelope. - + Args: envelope_id: UUID of the envelope updates: Fields to update (name, participants, etc.) - + Returns: Updated envelope data """ - return self._request('PATCH', f'/sign/envelopes/{envelope_id}', json_data=updates) - + return self._request("PATCH", f"/sign/envelopes/{envelope_id}", json_data=updates) + def delete_envelope(self, envelope_id: str) -> None: """Delete an envelope by ID. - + Args: envelope_id: UUID of the envelope """ - headers = {'Authorization': f'Bearer {self._get_token()}'} - response = requests.delete( - f'{self.base_url}/sign/envelopes/{envelope_id}', - headers=headers - ) + headers = {"Authorization": f"Bearer {self._get_token()}"} + response = requests.delete(f"{self.base_url}/sign/envelopes/{envelope_id}", headers=headers) response.raise_for_status() # ========== Document Management ========== - + def create_document( - self, - envelope_id: str, - file_path: Path, - document_name: str = None + self, envelope_id: str, file_path: Path, document_name: str | None = None ) -> dict[str, Any]: """Upload a document to an envelope using form-data. - + Args: envelope_id: ID of the envelope file_path: Path to the PDF file to upload document_name: Optional custom name for the document - + Returns: Created document with ID """ if document_name is None: document_name = file_path.name - + # Read the binary content of the PDF file - with open(file_path, 'rb') as f: + with file_path.open("rb") as f: pdf_binary = f.read() - + # Prepare metadata as JSON string - import json - metadata = json.dumps({'name': document_name}) - + metadata = json.dumps({"name": document_name}) + # Prepare form-data with binary content files = { - 'metadata': ('metadata', metadata, 'application/json'), - 'payload': (file_path.name, pdf_binary, 'application/pdf') + "metadata": ("metadata", metadata, "application/json"), + "payload": (file_path.name, pdf_binary, "application/pdf"), } - - headers = {'Authorization': f'Bearer {self._get_token()}'} - + + headers = {"Authorization": f"Bearer {self._get_token()}"} + response = requests.post( - f'{self.base_url}/sign/envelopes/{envelope_id}/documents', - headers=headers, - files=files + f"{self.base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files ) - + response.raise_for_status() return response.json() - + # ========== Participant Management ========== - + def create_participant( - self, - envelope_id: str, - participant_data: dict[str, Any] + self, envelope_id: str, participant_data: dict[str, Any] ) -> dict[str, Any]: """Add a participant to an envelope. - + Args: envelope_id: ID of the envelope participant_data: Participant configuration with role, email, name - + Returns: Created participant with ID """ return self._request( - 'POST', - f'/sign/envelopes/{envelope_id}/participants', - json_data=participant_data + "POST", f"/sign/envelopes/{envelope_id}/participants", json_data=participant_data ) - + # ========== Field Management ========== - + def create_field( - self, - envelope_id: str, - document_id: str, - field_data: dict[str, Any] + self, envelope_id: str, document_id: str, field_data: dict[str, Any] ) -> dict[str, Any]: """Add a signature field to a document in an envelope. - + Args: envelope_id: ID of the envelope document_id: ID of the document field_data: Field configuration with boundingBox, participantID, type, page - + Returns: Created field with ID """ return self._request( - 'POST', - f'/sign/envelopes/{envelope_id}/documents/{document_id}/fields', - json_data=field_data + "POST", + f"/sign/envelopes/{envelope_id}/documents/{document_id}/fields", + json_data=field_data, ) - + # ========== Envelope Actions ========== - + def send_for_signing(self, envelope_id: str) -> dict[str, Any]: """Send envelope to participants for signing. - + This transitions the envelope from 'drafted' to 'sent' status. - + Args: envelope_id: UUID of the envelope - + Returns: Updated envelope with 'sent' status """ # The correct endpoint uses a colon before 'send-for-signing' - return self._request('POST', f'/sign/envelopes/{envelope_id}:send-for-signing') - + return self._request("POST", f"/sign/envelopes/{envelope_id}:send-for-signing") + def cancel_envelope(self, envelope_id: str) -> dict[str, Any]: """Cancel an envelope that was sent for signing. - + Args: envelope_id: UUID of the envelope - + Returns: Envelope with 'cancelled' status """ - return self._request('PUT', f'/sign/envelopes/{envelope_id}/cancel') - + return self._request("PUT", f"/sign/envelopes/{envelope_id}/cancel") + def send_reminders(self, envelope_id: str) -> dict[str, Any]: """Send reminder notifications to pending signers. - + Args: envelope_id: UUID of the envelope - + Returns: Confirmation of reminder sent """ - return self._request('POST', f'/sign/envelopes/{envelope_id}/reminders') - + return self._request("POST", f"/sign/envelopes/{envelope_id}/reminders") + # ========== Document Downloads ========== - + def download_sealed_envelope(self, envelope_id: str) -> bytes: """Download the sealed (signed and completed) envelope. - + Args: envelope_id: UUID of the envelope - + Returns: PDF bytes of the sealed document """ # The correct endpoint uses a colon before 'download-sealed' - return self._request_bytes('GET', f'/sign/envelopes/{envelope_id}:download-sealed') - + return self._request_bytes("GET", f"/sign/envelopes/{envelope_id}:download-sealed") + def download_original_envelope(self, envelope_id: str) -> bytes: """Download the original (unsigned) envelope documents. - + Args: envelope_id: UUID of the envelope - + Returns: Original document bytes """ # The correct endpoint uses a colon before 'download-original' - return self._request_bytes('GET', f'/sign/envelopes/{envelope_id}:download-original') + return self._request_bytes("GET", f"/sign/envelopes/{envelope_id}:download-original") diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index 9dcc87c..7d4ebe8 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -4,17 +4,17 @@ ============================= The script exemplifies a typical workflow for document format standardization. -As a IT administrator, it's essential to convert -large collections of documents into standardized formats for archival, compliance, -or system integration purposes. Manually converting individual files through desktop -applications is time-consuming and impractical for large document sets, often leading +As a IT administrator, it's essential to convert +large collections of documents into standardized formats for archival, compliance, +or system integration purposes. Manually converting individual files through desktop +applications is time-consuming and impractical for large document sets, often leading to inconsistent results and wasted effort. -This workflow automates bulk document conversion. The script processes files -individually - for every document matching the specified pattern in the input folder, -it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using -high-fidelity conversion algorithms. Each converted file is saved to the output -folder with the same base name but the new format extension, resulting in a +This workflow automates bulk document conversion. The script processes files +individually - for every document matching the specified pattern in the input folder, +it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using +high-fidelity conversion algorithms. Each converted file is saved to the output +folder with the same base name but the new format extension, resulting in a complete batch of standardized documents. BATCH CONVERSION FEATURES: @@ -31,64 +31,64 @@ import sys from pathlib import Path + from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup - # Supported output formats -SUPPORTED_FORMATS = ['pdf', 'docx', 'xlsx', 'pptx'] +SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx"] -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) < 4: print("Usage: python batch_process.py [pattern]") print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") print("Example: python batch_process.py ./docs ./output pdf '*.docx'") sys.exit(1) - + # Get folder paths and format from arguments input_folder = Path(sys.argv[1]) output_folder = Path(sys.argv[2]) to_format = sys.argv[3].lower() pattern = sys.argv[4] if len(sys.argv) > 4 else "*" - + # Validate output format if to_format not in SUPPORTED_FORMATS: print(f"❌ Error: Unsupported format '{to_format}'") print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") sys.exit(1) - + # Validate and setup with custom pattern files = validate_and_setup(input_folder, output_folder, file_patterns=[pattern]) print(f"📋 Found {len(files)} file(s) matching '{pattern}'\n") - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + # Process each document success_count = 0 failed_count = 0 - + for i, file_path in enumerate(files, 1): print(f"[{i}/{len(files)}] Processing: {file_path.name}") - + try: # Convert to target format print(f" 🔄 Converting to {to_format.upper()}...") converted = client.convert(file_path, to_format) - + # Save converted file output_file = output_folder / f"{file_path.stem}.{to_format}" output_file.write_bytes(converted) - + print(f" ✅ Converted: {output_file.name}\n") success_count += 1 - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f" ❌ FAILED: {e}\n") failed_count += 1 - + # Display summary print("=" * 60) print(f"✅ {success_count} file(s) converted to {to_format.upper()}") diff --git a/samples/python/bulk_password_protect.py b/samples/python/bulk_password_protect.py index f1484ae..a8c76b0 100644 --- a/samples/python/bulk_password_protect.py +++ b/samples/python/bulk_password_protect.py @@ -4,17 +4,17 @@ ============================ The script exemplifies a typical workflow for securing confidential documents. -As a security professional, it's essential to protect sensitive -documents with passwords before distributing them to authorized personnel, storing -them in shared drives, or archiving them for compliance purposes. Manually setting -passwords on individual files is tedious and inconsistent, leading to weak passwords +As a security professional, it's essential to protect sensitive +documents with passwords before distributing them to authorized personnel, storing +them in shared drives, or archiving them for compliance purposes. Manually setting +passwords on individual files is tedious and inconsistent, leading to weak passwords or missed files that remain unprotected. -This workflow automates secure document protection. The script processes each PDF -file individually - for every document in the input folder, it applies robust -password encryption using a consistent password across all files. Each protected -file is saved to the output folder with the same filename, ensuring that the entire -batch of documents maintains uniform security standards. The result is a complete +This workflow automates secure document protection. The script processes each PDF +file individually - for every document in the input folder, it applies robust +password encryption using a consistent password across all files. Each protected +file is saved to the output folder with the same filename, ensuring that the entire +batch of documents maintains uniform security standards. The result is a complete set of password-protected PDFs ready for secure distribution or storage. DOCUMENT SECURITY STANDARDS: @@ -31,56 +31,57 @@ import sys from pathlib import Path + from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) != 4: print("Usage: python bulk_password_protect.py ") sys.exit(1) - + # Get folder paths and password from arguments input_folder = Path(sys.argv[1]) output_folder = Path(sys.argv[2]) password = sys.argv[3] - + # Validate password strength if len(password) < 6: print("❌ Error: Password must be at least 6 characters long") sys.exit(1) - + # Validate and setup (only process PDF files) - files = validate_and_setup(input_folder, output_folder, file_patterns=['*.pdf']) + files = validate_and_setup(input_folder, output_folder, file_patterns=["*.pdf"]) print(f"📋 Found {len(files)} PDF document(s) to protect\n") - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + # Process each document success_count = 0 failed_count = 0 - + for i, pdf_file in enumerate(files, 1): print(f"[{i}/{len(files)}] Processing: {pdf_file.name}") - + try: # Apply password protection print(" 🔐 Applying password protection...") protected_pdf = client.password_protect(pdf_file, password) - + # Save protected PDF output_file = output_folder / pdf_file.name output_file.write_bytes(protected_pdf) - + print(f" ✅ Protected: {output_file.name}\n") success_count += 1 - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f" ❌ FAILED: {e}\n") failed_count += 1 - + # Display summary print("=" * 60) print(f"✅ {success_count} document(s) password protected") diff --git a/samples/python/convert_cli.py b/samples/python/convert_cli.py index 0df883e..bd546b8 100644 --- a/samples/python/convert_cli.py +++ b/samples/python/convert_cli.py @@ -4,15 +4,15 @@ ============================== The script exemplifies a typical workflow for quick document format conversion. -As a business professional, you often need to convert individual -documents between formats for sharing, presentations, or compatibility requirements. -Whether converting a Word document to PDF for distribution, an Excel spreadsheet to -CSV for data processing, or a presentation to images for web display, manual +As a business professional, you often need to convert individual +documents between formats for sharing, presentations, or compatibility requirements. +Whether converting a Word document to PDF for distribution, an Excel spreadsheet to +CSV for data processing, or a presentation to images for web display, manual conversion through multiple applications is inefficient. -This workflow provides instant document conversion. The script takes a single input -file and converts it to the specified output format using professional-grade -conversion algorithms. The result is a high-quality converted file that preserves +This workflow provides instant document conversion. The script takes a single input +file and converts it to the specified output format using professional-grade +conversion algorithms. The result is a high-quality converted file that preserves formatting, structure, and content fidelity, ready for immediate use. CONVERSION FEATURES: @@ -30,58 +30,58 @@ import sys from pathlib import Path -from api.platform_api import PlatformAPIClient +from api.platform_api import PlatformAPIClient # Supported output formats -SUPPORTED_FORMATS = ['pdf', 'docx', 'xlsx', 'pptx', 'png'] +SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx", "png"] -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) != 4: print("Usage: python convert_cli.py ") print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") print("Example: python convert_cli.py document.docx document.pdf pdf") sys.exit(1) - + # Get file paths and format from arguments input_path = Path(sys.argv[1]) output_path = Path(sys.argv[2]) to_format = sys.argv[3].lower() - + # Validate input file exists if not input_path.exists(): print(f"❌ Error: Input file not found: {input_path}") sys.exit(1) - + # Validate output format if to_format not in SUPPORTED_FORMATS: print(f"❌ Error: Unsupported format '{to_format}'") print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") sys.exit(1) - + # Create output directory if needed output_path.parent.mkdir(parents=True, exist_ok=True) - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + try: # Convert document print(f"🔄 Converting {input_path.name} to {to_format.upper()}...") converted = client.convert(input_path, to_format) - + # Save converted file output_path.write_bytes(converted) - + # Display success message - print(f"✅ Conversion successful!") + print("✅ Conversion successful!") print(f"📄 Input: {input_path.name} ({input_path.stat().st_size:,} bytes)") print(f"📄 Output: {output_path.name} ({len(converted):,} bytes)") print(f"📂 Saved to: {output_path.absolute()}") - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f"❌ Conversion FAILED: {e}") sys.exit(1) diff --git a/samples/python/employee_policy_onboarding.py b/samples/python/employee_policy_onboarding.py index ad7066e..c516f33 100644 --- a/samples/python/employee_policy_onboarding.py +++ b/samples/python/employee_policy_onboarding.py @@ -4,22 +4,22 @@ ============================== This script exemplifies a typical HR onboarding workflow for new employees. -As an HR professional, it's necessary to ensure all new hires review and sign -required company policies before their start date. Manual distribution and -tracking of signatures is time-consuming and error-prone, especially when +As an HR professional, it's necessary to ensure all new hires review and sign +required company policies before their start date. Manual distribution and +tracking of signatures is time-consuming and error-prone, especially when onboarding multiple employees simultaneously. -This workflow automates policy distribution and signature collection. The script -processes each new employee individually - for every person in the CSV file, it -creates a signature envelope containing all company policy documents, sends it -via email with signature fields pre-configured, monitors the signing status, and -automatically downloads the signed documents once completed. Each employee's -signed policies are organized in their own folder, creating an audit-ready +This workflow automates policy distribution and signature collection. The script +processes each new employee individually - for every person in the CSV file, it +creates a signature envelope containing all company policy documents, sends it +via email with signature fields pre-configured, monitors the signing status, and +automatically downloads the signed documents once completed. Each employee's +signed policies are organized in their own folder, creating an audit-ready archive of onboarding documentation. -NOTE: To run this script and see the complete workflow, you must provide a CSV file -with valid employee names and email addresses. The script will send actual signature +NOTE: To run this script and see the complete workflow, you must provide a CSV file +with valid employee names and email addresses. The script will send actual signature requests to these email addresses and wait for them to be signed. EMPLOYEE CSV FORMAT: @@ -46,133 +46,143 @@ """ import sys -import csv -import time -import json -import base64 from pathlib import Path + from api.sign_api import SignAPIClient from helper_functions.sign_helpers import ( - load_employees_from_csv, - load_policy_documents_from_folder, + add_signature_fields_to_documents, create_employee_folder_name, create_signature_envelope, - add_signature_fields_to_documents, - send_and_monitor_envelope, download_signed_document, - log_step + load_employees_from_csv, + load_policy_documents_from_folder, + log_step, + send_and_monitor_envelope, ) -def main(): + +class EnvelopeNotSignedError(Exception): + """Raised when an envelope was not signed in time.""" + + +def main() -> None: # Check command-line arguments if len(sys.argv) != 3: - print('Usage: python employee_policy_onboarding.py ') + print("Usage: python employee_policy_onboarding.py ") sys.exit(1) - + # Parse arguments policies_folder = Path(sys.argv[1]) employees_csv = Path(sys.argv[2]) - output_folder = Path('output') - + output_folder = Path("output") + # Validate inputs if not policies_folder.exists() or not policies_folder.is_dir(): - print(f'❌ Policies folder not found: {policies_folder}') + print(f"❌ Policies folder not found: {policies_folder}") sys.exit(1) - + if not employees_csv.exists(): - print(f'❌ Employees CSV not found: {employees_csv}') + print(f"❌ Employees CSV not found: {employees_csv}") sys.exit(1) - + # Display header - print('=' * 60) - print('📝 SEND POLICIES TO EMPLOYEES') - print('=' * 60) - print(f'Policies: {policies_folder}') - print(f'Employees: {employees_csv}') - print(f'Output: {output_folder}') - print('=' * 60) + print("=" * 60) + print("📝 SEND POLICIES TO EMPLOYEES") + print("=" * 60) + print(f"Policies: {policies_folder}") + print(f"Employees: {employees_csv}") + print(f"Output: {output_folder}") + print("=" * 60) print() - + try: # Load employees and documents employees = load_employees_from_csv(employees_csv) - print(f'👥 Found {len(employees)} employee(s)\n') - + print(f"👥 Found {len(employees)} employee(s)\n") + documents = load_policy_documents_from_folder(policies_folder) - + # Create output folder output_folder.mkdir(parents=True, exist_ok=True) - + # Initialize Sign API client sign_client = SignAPIClient() - + # Process each employee - print('=' * 60) - print(f'📤 PROCESSING {len(employees)} EMPLOYEE(S)') - print('=' * 60) - + print("=" * 60) + print(f"📤 PROCESSING {len(employees)} EMPLOYEE(S)") + print("=" * 60) + success_count = 0 failed_count = 0 - + for i, employee in enumerate(employees, 1): - name = employee['name'] - email = employee['email'] - + name = employee["name"] + email = employee["email"] + print(f"\n[{i}/{len(employees)}] {name}") - + try: # Create employee-specific output folder folder_name = create_employee_folder_name(name) employee_folder = output_folder / folder_name employee_folder.mkdir(parents=True, exist_ok=True) - + # Create envelope and upload documents - log_step('📝 Creating envelope...') - envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) - + log_step("📝 Creating envelope...") + envelope_id, document_ids = create_signature_envelope( + sign_client, documents, name, email + ) + # Add participant (signer) - log_step('👤 Adding signer...') - participant_data = {'email': email, 'role': 'signer', 'name': name} + log_step("👤 Adding signer...") + participant_data = {"email": email, "role": "signer", "name": name} participant = sign_client.create_participant(envelope_id, participant_data) - participant_id = participant['ID'] - + participant_id = participant["ID"] + # Add signature fields to all documents - log_step('✍️ Adding fields...') - add_signature_fields_to_documents(sign_client, envelope_id, document_ids, participant_id) - + log_step("✍️ Adding fields...") + add_signature_fields_to_documents( + sign_client, envelope_id, document_ids, participant_id + ) + # Send and monitor envelope - log_step('📤 Sending...') - status = send_and_monitor_envelope(sign_client, envelope_id, email, timeout_minutes=60) - - if status != 'sealed': - raise Exception(f'Envelope not signed: {status}') - + log_step("📤 Sending...") + status = send_and_monitor_envelope( + sign_client, envelope_id, email, timeout_minutes=60 + ) + + if status != "sealed": + raise EnvelopeNotSignedError(f"Envelope not signed: {status}") # noqa: TRY301 + # Download signed documents - log_step('📥 Downloading...') - download_signed_document(sign_client, envelope_id, employee_folder, 'signed-policies.zip') - - print(f" ✅ Completed\n") + log_step("📥 Downloading...") + download_signed_document( + sign_client, envelope_id, employee_folder, "signed-policies.zip" + ) + + print(" ✅ Completed\n") success_count += 1 - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f" ❌ FAILED: {e}\n") failed_count += 1 - + # Display summary - print('=' * 60) + print("=" * 60) print(f"✅ {success_count} employee(s) completed") if failed_count > 0: print(f"❌ {failed_count} failed") print(f"📂 Output: {output_folder.absolute()}") - print('=' * 60) - + print("=" * 60) + except KeyboardInterrupt: - print('\n\n⚠️ Interrupted by user') + print("\n\n⚠️ Interrupted by user") sys.exit(1) - except Exception as e: - print(f'\n❌ Error: {e}') + except Exception as e: # noqa: BLE001 + print(f"\n❌ Error: {e}") sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/samples/python/extract_data.py b/samples/python/extract_data.py index d5502ad..7ec7c35 100644 --- a/samples/python/extract_data.py +++ b/samples/python/extract_data.py @@ -4,16 +4,16 @@ ============================ The script exemplifies a typical workflow for intelligent document data extraction. -As a data analyst, you need to extract structured -data from PDF documents - whether form fields from applications, surveys, and -questionnaires, or table data from reports, invoices, and financial statements. -Manual data entry is error-prone and time-consuming, especially when processing +As a data analyst, you need to extract structured +data from PDF documents - whether form fields from applications, surveys, and +questionnaires, or table data from reports, invoices, and financial statements. +Manual data entry is error-prone and time-consuming, especially when processing hundreds of documents for analysis or database import. -This workflow automates data extraction using AI-powered document understanding. -The script analyzes PDF documents and intelligently identifies and extracts either -form fields (with field names and values) or table structures (with rows, columns, -and cell contents). The extracted data is saved as structured JSON, ready for +This workflow automates data extraction using AI-powered document understanding. +The script analyzes PDF documents and intelligently identifies and extracts either +form fields (with field names and values) or table structures (with rows, columns, +and cell contents). The extracted data is saved as structured JSON, ready for immediate integration with databases, spreadsheets, or analytics pipelines. DATA EXTRACTION FEATURES: @@ -34,76 +34,77 @@ python extract_data.py tables invoice.pdf tables.json """ -import sys import json +import sys from pathlib import Path + from api.platform_api import PlatformAPIClient -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) != 4: print("Usage: python extract_data.py ") print("Modes: forms, tables") print("Example: python extract_data.py forms application.pdf data.json") sys.exit(1) - + # Get mode and file paths from arguments mode = sys.argv[1].lower() input_path = Path(sys.argv[2]) output_path = Path(sys.argv[3]) - + # Validate mode - if mode not in ['forms', 'tables']: + if mode not in ["forms", "tables"]: print("❌ Error: Mode must be 'forms' or 'tables'") sys.exit(1) - + # Validate input file exists if not input_path.exists(): print(f"❌ Error: Input file not found: {input_path}") sys.exit(1) - + # Validate input is a PDF - if input_path.suffix.lower() != '.pdf': - print(f"❌ Error: Input must be a PDF file") + if input_path.suffix.lower() != ".pdf": + print("❌ Error: Input must be a PDF file") sys.exit(1) - + # Create output directory if needed output_path.parent.mkdir(parents=True, exist_ok=True) - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + try: # Extract data based on mode if mode == "forms": print(f"📋 Extracting form fields from {input_path.name}...") data = client.extract_forms(input_path) data_type = "form fields" - + else: # mode == "tables" print(f"📊 Extracting table data from {input_path.name}...") data = client.extract_tables(input_path) data_type = "tables" - + # Count extracted items - result = data.get('result', {}) + result = data.get("result", {}) if mode == "forms": - item_count = len(result.get('fields', [])) + item_count = len(result.get("fields", [])) else: - item_count = len(result.get('tables', [])) - + item_count = len(result.get("tables", [])) + # Save extracted data as JSON output_path.write_text(json.dumps(data, indent=2)) - + # Display success message - print(f"✅ Extraction successful!") + print("✅ Extraction successful!") print(f"📊 Extracted: {item_count} {data_type}") print(f"📄 Input: {input_path.name}") print(f"📄 Output: {output_path.name}") print(f"📂 Saved to: {output_path.absolute()}") - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f"❌ Extraction FAILED: {e}") sys.exit(1) diff --git a/samples/python/helper_functions/document_helpers.py b/samples/python/helper_functions/document_helpers.py index 18c5141..6879a0e 100644 --- a/samples/python/helper_functions/document_helpers.py +++ b/samples/python/helper_functions/document_helpers.py @@ -2,42 +2,46 @@ Common helper utilities for document processing scripts. """ +from __future__ import annotations + import sys -from pathlib import Path +from pathlib import Path # noqa: TC003 -def validate_and_setup(input_folder, output_folder, file_patterns=None): +def validate_and_setup( + input_folder: Path, output_folder: Path, file_patterns: list[str] | None = None +) -> list[Path]: """ Validate input folder and setup output folder. Returns list of files to process. - + Args: input_folder: Path to the input directory containing files to process output_folder: Path to the output directory for processed files file_patterns: List of glob patterns to match files (e.g., ['*.docx', '*.pdf']) If None, defaults to common Office document formats - + Returns: List of Path objects for files matching the patterns """ # Default patterns for Office documents if none provided if file_patterns is None: - file_patterns = ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt'] - + file_patterns = ["*.docx", "*.doc", "*.xlsx", "*.xls", "*.pptx", "*.ppt"] + # Validate input folder exists if not input_folder.exists() or not input_folder.is_dir(): print(f"❌ Error: Invalid input folder: {input_folder}") sys.exit(1) - + # Create output folder if needed output_folder.mkdir(parents=True, exist_ok=True) - + # Find all matching files files = [] for pattern in file_patterns: files.extend(input_folder.glob(pattern)) - + if not files: print(f"❌ No files matching patterns {file_patterns} found in {input_folder}") sys.exit(1) - + return files diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py index 3398554..2540876 100644 --- a/samples/python/helper_functions/sign_helpers.py +++ b/samples/python/helper_functions/sign_helpers.py @@ -6,151 +6,165 @@ import json import time import zipfile -from pathlib import Path from datetime import datetime +from pathlib import Path # noqa: TC003 +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from api.sign_api import SignAPIClient def create_employee_folder_name(employee_name: str) -> str: """Convert employee name to folder-safe name. - + Args: employee_name: Full name like "John Doe" - + Returns: Folder-safe name like "john-doe" """ - return employee_name.lower().replace(' ', '-').replace('.', '') + return employee_name.lower().replace(" ", "-").replace(".", "") def load_employees_from_csv(csv_path: Path) -> list[dict]: """Load employee list from CSV file. - + Args: csv_path: Path to CSV file with columns: name, email - + Returns: List of employee dictionaries with 'name' and 'email' - + Raises: ValueError: If CSV format is invalid """ if not csv_path.exists(): - raise ValueError(f'CSV file not found: {csv_path}') - + raise ValueError(f"CSV file not found: {csv_path}") + employees = [] - - with open(csv_path, 'r', encoding='utf-8') as f: + + with csv_path.open(encoding="utf-8") as f: reader = csv.DictReader(f) - + # Validate CSV has required columns - if 'name' not in reader.fieldnames or 'email' not in reader.fieldnames: + if "name" not in reader.fieldnames or "email" not in reader.fieldnames: raise ValueError('CSV must have "name" and "email" columns') - + for row in reader: - name = row['name'].strip() - email = row['email'].strip() - - if name and email and '@' in email: - employees.append({'name': name, 'email': email}) - + name = row["name"].strip() + email = row["email"].strip() + + if name and email and "@" in email: + employees.append({"name": name, "email": email}) + if not employees: - raise ValueError('No valid employees found in CSV') - + raise ValueError("No valid employees found in CSV") + return employees def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: """Load all PDF files from the policies folder. - + Args: policies_folder: Path to folder containing policy PDFs - + Returns: List of document dictionaries with 'name', 'binary', and 'path' """ - policy_files = list(policies_folder.glob('*.pdf')) - + policy_files = list(policies_folder.glob("*.pdf")) + if not policy_files: - raise ValueError(f'No PDF files found in {policies_folder}') - - print(f'📂 Found {len(policy_files)} policy document(s)\n') - + raise ValueError(f"No PDF files found in {policies_folder}") + + print(f"📂 Found {len(policy_files)} policy document(s)\n") + documents = [] for pf in policy_files: - with open(pf, 'rb') as f: + with pf.open("rb") as f: binary_data = f.read() - - documents.append({ - 'name': pf.name, - 'binary': binary_data, - 'path': str(pf) - }) - + + documents.append({"name": pf.name, "binary": binary_data, "path": str(pf)}) + return documents -def create_signature_envelope(sign_client, documents: list[dict], employee_name: str, employee_email: str) -> tuple[str, list[str]]: +def create_signature_envelope( + sign_client: SignAPIClient, + documents: list[dict], + employee_name: str, + _employee_email: str, +) -> tuple[str, list[str]]: """Create envelope and upload documents. - + Args: sign_client: Sign API client instance documents: List of document dicts with 'name', 'binary', 'path' employee_name: Full name of employee employee_email: Email address of employee - + Returns: Tuple of (envelope_id, list of document_ids) """ # Create empty envelope envelope_data = { - 'name': f'Company Policies - {employee_name}', - 'mode': "parallel", - 'notification': { - 'subject': f'Please sign: Company Policies', - 'body': f'Hello {employee_name}, please review and sign the attached company policy documents.' - } + "name": f"Company Policies - {employee_name}", + "mode": "parallel", + "notification": { + "subject": "Please sign: Company Policies", + "body": ( + f"Hello {employee_name}, please review and sign the attached " + "company policy documents." + ), + }, } - + envelope = sign_client.create_envelope(envelope_data) - envelope_id = envelope['ID'] - + envelope_id = envelope["ID"] + # Upload documents to envelope document_ids = [] - + for doc in documents: - doc_name = doc['name'] - doc_binary = doc['binary'] - + doc_name = doc["name"] + doc_binary = doc["binary"] + # Prepare metadata as JSON string - metadata = json.dumps({'name': doc_name}) - + metadata = json.dumps({"name": doc_name}) + # Prepare form-data with binary content files = { - 'metadata': ('metadata', metadata, 'application/json'), - 'payload': (doc_name, doc_binary, 'application/pdf') + "metadata": ("metadata", metadata, "application/json"), + "payload": (doc_name, doc_binary, "application/pdf"), } - - headers = {'Authorization': f'Bearer {sign_client._get_token()}'} - + + headers = {"Authorization": f"Bearer {sign_client._get_token()}"} + import requests - response = requests.post( - f'{sign_client.base_url}/sign/envelopes/{envelope_id}/documents', + + response = requests.post( # noqa: S113 + f"{sign_client.base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, - files=files + files=files, ) - + response.raise_for_status() document = response.json() - - document_id = document['ID'] + + document_id = document["ID"] document_ids.append(document_id) - + return envelope_id, document_ids -def add_signature_fields_to_documents(sign_client, envelope_id: str, document_ids: list[str], participant_id: str) -> None: +def add_signature_fields_to_documents( + sign_client: SignAPIClient, + envelope_id: str, + document_ids: list[str], + participant_id: str, +) -> None: """Add signature and date fields to all documents in envelope. - + Args: sign_client: Sign API client instance envelope_id: ID of the envelope @@ -160,153 +174,159 @@ def add_signature_fields_to_documents(sign_client, envelope_id: str, document_id for doc_id in document_ids: # Add signature field signature_field_data = { - 'participantID': participant_id, - 'type': 'signature', - 'label': 'Your Signature', - 'page': 1, - 'boundingBox': [200, 300, 60, 40], - 'required': True + "participantID": participant_id, + "type": "signature", + "label": "Your Signature", + "page": 1, + "boundingBox": [200, 300, 60, 40], + "required": True, } sign_client.create_field(envelope_id, doc_id, signature_field_data) - + # Add date field date_field_data = { - 'participantID': participant_id, - 'type': 'date', - 'label': 'Date Signed', - 'page': 1, - 'boundingBox': [320, 650, 150, 50], - 'required': True, - 'format': 'MM/DD/YYYY' + "participantID": participant_id, + "type": "date", + "label": "Date Signed", + "page": 1, + "boundingBox": [320, 650, 150, 50], + "required": True, + "format": "MM/DD/YYYY", } sign_client.create_field(envelope_id, doc_id, date_field_data) -def send_and_monitor_envelope(sign_client, envelope_id: str, email: str, timeout_minutes: int = 60) -> str: +def send_and_monitor_envelope( + sign_client: SignAPIClient, envelope_id: str, email: str, timeout_minutes: int = 60 +) -> str: """Send envelope and monitor until signed or timeout. - + Args: sign_client: Sign API client instance envelope_id: ID of envelope to send email: Email address of recipient timeout_minutes: Maximum time to wait - + Returns: Final status: 'sealed', 'cancelled', 'timeout', or 'error' """ # Send envelope sign_client.send_for_signing(envelope_id) - + # Log send time and status - send_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - print(f' ✅ Sent at: {send_time}') - print(f' 📧 Email sent to: {email}') - print(f' 🔗 Envelope ID: {envelope_id}') - + send_time = datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d %H:%M:%S") + print(f" ✅ Sent at: {send_time}") + print(f" 📧 Email sent to: {email}") + print(f" 🔗 Envelope ID: {envelope_id}") + # Check initial status envelope = sign_client.get_envelope(envelope_id) - print(f' 📊 Status: {envelope["status"]}') - + print(f" 📊 Status: {envelope['status']}") + # Monitor for completion - print(f' ⏳ Waiting for signature...') - print(f' ⏱️ Checking every 30 seconds (timeout: {timeout_minutes} minutes)') - + print(" ⏳ Waiting for signature...") + print(f" ⏱️ Checking every 30 seconds (timeout: {timeout_minutes} minutes)") + status = monitor_envelope(sign_client, envelope_id, timeout_minutes) - + # Log final status - completion_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - print(f' 📊 Final status: {status}') - print(f' 🕐 Completed at: {completion_time}') - + completion_time = datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d %H:%M:%S") + print(f" 📊 Final status: {status}") + print(f" 🕐 Completed at: {completion_time}") + return status -def monitor_envelope(sign_client, envelope_id: str, timeout_minutes: int = 60) -> str: +def monitor_envelope( + sign_client: SignAPIClient, envelope_id: str, timeout_minutes: int = 60 +) -> str: """Monitor envelope until signed, cancelled, or timeout. - + Args: sign_client: Sign API client instance envelope_id: ID of envelope to monitor timeout_minutes: Maximum time to wait - + Returns: Final status: 'sealed', 'cancelled', 'timeout', or 'error' """ check_interval = 30 # seconds max_checks = (timeout_minutes * 60) // check_interval - + for i in range(max_checks): try: envelope = sign_client.get_envelope(envelope_id) - status = envelope['status'] - - if status == 'sealed': - return 'sealed' - elif status in ['cancelled', 'rejected', 'deleted']: - return 'cancelled' - + status = envelope["status"] + + if status == "sealed": + return "sealed" + if status in ["cancelled", "rejected", "deleted"]: + return "cancelled" + if i < max_checks - 1: time.sleep(check_interval) - - except Exception as e: - print(f' ⚠️ Error checking status: {e}') - return 'error' - - return 'timeout' + except Exception as e: # noqa: BLE001 + print(f" ⚠️ Error checking status: {e}") + return "error" + + return "timeout" -def download_signed_document(sign_client, envelope_id: str, output_folder: Path, document_name: str) -> Path: + +def download_signed_document( + sign_client: SignAPIClient, envelope_id: str, output_folder: Path, document_name: str +) -> Path: """Download sealed envelope and extract signed documents. - + The API returns a ZIP file containing: - All signed PDF documents - Audit trail document - + This function extracts the contents and removes the ZIP file. - + Args: sign_client: Sign API client instance envelope_id: ID of sealed envelope output_folder: Employee-specific output folder document_name: Name for the saved file (should end with .zip) - + Returns: Path to extracted documents folder """ # Download sealed envelope (returns ZIP file) zip_bytes = sign_client.download_sealed_envelope(envelope_id) - + # Save as temporary ZIP file - if not document_name.endswith('.zip'): - document_name = document_name.replace('.pdf', '.zip') - + if not document_name.endswith(".zip"): + document_name = document_name.replace(".pdf", ".zip") + temp_zip_path = output_folder / document_name temp_zip_path.write_bytes(zip_bytes) - + # Extract the ZIP contents - extract_folder = output_folder / 'signed-documents' + extract_folder = output_folder / "signed-documents" extract_folder.mkdir(exist_ok=True) - - with zipfile.ZipFile(temp_zip_path, 'r') as zip_ref: + + with zipfile.ZipFile(temp_zip_path, "r") as zip_ref: zip_ref.extractall(extract_folder) - + # Delete the ZIP file after extraction temp_zip_path.unlink() - + # Save envelope metadata envelope = sign_client.get_envelope(envelope_id) - json_path = output_folder / 'envelope-info.json' + json_path = output_folder / "envelope-info.json" json_path.write_text(json.dumps(envelope, indent=2)) - - print(f' 💾 Extracted to: {extract_folder}') - + + print(f" 💾 Extracted to: {extract_folder}") + return extract_folder def log_step(message: str) -> None: """Log a processing step with consistent formatting. - + Args: message: The message to log """ - print(f' {message}') + print(f" {message}") diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index bc538a7..f38aafc 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -4,16 +4,16 @@ ================================ The script exemplifies a typical workflow of marketing brochure distribution. -As a marketing professional, it's necessary to share company brochures externally -while ensuring they comply with corporate distribution standards. Word document -properties can expose internal information such as author names, template paths, +As a marketing professional, it's necessary to share company brochures externally +while ensuring they comply with corporate distribution standards. Word document +properties can expose internal information such as author names, template paths, revision history, and company file structures that should remain confidential. -This workflow automates compliant document preparation. The script processes each -file individually - for every brochure in the input folder, it converts the Word -document into PDF format, then compresses the file to reduce size and optimize -transmission, and finally removes all metadata properties to ensure privacy and -confidentiality. Each processed file is saved to the output folder, resulting in +This workflow automates compliant document preparation. The script processes each +file individually - for every brochure in the input folder, it converts the Word +document into PDF format, then compresses the file to reduce size and optimize +transmission, and finally removes all metadata properties to ensure privacy and +confidentiality. Each processed file is saved to the output folder, resulting in distribution-ready brochures. COMPANY DISTRIBUTION STANDARDS: @@ -32,74 +32,74 @@ import sys from pathlib import Path + from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup - # Configuration: Properties to remove from PDFs PROPERTIES_TO_REMOVE = ["title", "author", "subject", "keywords", "creator", "producer"] -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) != 3: print("Usage: python prepare_pdf_for_distribution.py ") sys.exit(1) - + # Get folder paths from arguments input_folder = Path(sys.argv[1]) output_folder = Path(sys.argv[2]) - + # Validate and setup files = validate_and_setup(input_folder, output_folder) print(f"📋 Found {len(files)} document(s) to process\n") - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + # Process each document success_count = 0 failed_count = 0 - + for i, doc in enumerate(files, 1): print(f"[{i}/{len(files)}] Processing: {doc.name}") - + temp_pdf = None try: # Step 1: Convert to PDF print(" 🔐 Converting to PDF...") pdf_bytes = client.convert(doc, "pdf") - + temp_pdf = output_folder / f"{doc.stem}_temp.pdf" temp_pdf.write_bytes(pdf_bytes) - - + # Step 2: Compress PDF print(" 📦 Compressing...") compressed_pdf = client.compress(temp_pdf, level=2) - + temp_pdf.write_bytes(compressed_pdf) - + # Step 3: Remove metadata properties print(" 🔒 Removing metadata...") - properties_to_clear = {prop: "" for prop in PROPERTIES_TO_REMOVE} - clean_pdf = client._request_bytes("transformations", "set-properties", temp_pdf, properties_to_clear) - - + properties_to_clear = dict.fromkeys(PROPERTIES_TO_REMOVE, "") + clean_pdf = client._request_bytes( + "transformations", "set-properties", temp_pdf, properties_to_clear + ) + # Save final PDF final_pdf = output_folder / f"{doc.stem}.pdf" final_pdf.write_bytes(clean_pdf) temp_pdf.unlink() - + print(f" ✅ Secured: {final_pdf.name}\n") success_count += 1 - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f" ❌ FAILED: {e}\n") failed_count += 1 if temp_pdf and temp_pdf.exists(): temp_pdf.unlink() - + # Display summary print("=" * 60) print(f"✅ {success_count} document(s) secured") diff --git a/samples/python/pyproject.toml b/samples/python/pyproject.toml new file mode 100644 index 0000000..b424963 --- /dev/null +++ b/samples/python/pyproject.toml @@ -0,0 +1,111 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = [] + +[project] +name = "nitro-platform-samples" +version = "0.1.0" +description = "Nitro Platform API samples and examples" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "httpx>=0.27.0", + "python-dotenv>=1.0.0", + "reportlab>=4.0.0", +] +[tool.ruff] +format.preview = true +target-version = "py314" +line-length = 100 +lint.select = [ + "A", # builtins shadowing + "ANN", # type annotations + "ARG", # unused arguments + "B", # bugbear (common bugs & bad patterns) + "BLE", # blind excepts + "C4", # comprehensions + "DTZ", # datetime timezone + "E", # pycodestyle errors + "ERA", # eradicate commented-out code + "FA", # future annotations + "FBT", # boolean trap + "F", # pyflakes + "FURB", # refurb - modern Python idioms + "I", # import sorting + "ICN", # import conventions + "ISC", # implicit str concat + "N", # naming conventions + "NPY", # numpy rules + "PGH", # pygrep-hooks + "PTH", # pathlib + "PT", # pytest style + "RET", # return statements + "RUF", # Ruff extra rules + "TRY", # tryceratops + "S", # security + "SIM", # simplifications + "C90", # mccabe complexity + "TID", # tidy imports + "TC", # typing under TYPE_CHECKING + "TD", # TODOs must be annotated + "UP", # pyupgrade / newer syntax + "W", # pycodestyle warnings + "YTT", # flake8-2020 + "T10", # debugger statements +] +lint.ignore = ["S101", "S311", "TRY003", "TC006"] +lint.flake8-type-checking.strict = true + +[tool.pyright] +typeCheckingMode = "strict" + +# Pylint-------------------------------------------------------------------------- +# https://github.com/atlassian-api/atlassian-python-api/blob/master/pyproject.toml +# Pylint-------------------------------------------------------------------------- +[tool.pylint.FORMAT] +max-line-length = 100 + +[tool.pylint.MASTER] +jobs = 4 +ignore-paths = '.venv' + +[tool.pylint.REPORTS] +output-format = "colorized" +reports = "no" + +[tool.pylint.TYPECHECK] +generated-members = [] + +[tool.pylint.BASIC] +good-names-rgxs = [".*Dep$"] +good-names = [ + "i", + "j", + "n", +] + +# [tool.pylint.SPELLING] +# Spelling checker disabled - requires language dictionaries to be installed +# spelling-private-dict-file = "private-dictionary.txt" +# spelling-dict = "en_GB" + +[tool.pylint.DESIGN] +max-args = 7 +min-public-methods = 1 +max-positional-arguments = 5 + + +[tool.pylint.LOGGING] + +[tool.pylint.'MESSAGES CONTROL'] +disable = ["logging-fstring-interpolation", "wrong-import-order"] + +[project.optional-dependencies] +dev = [ + "pyenchant>=3.3.0", + "pylint>=4.0.4", + "pyright>=1.1.407", +] \ No newline at end of file diff --git a/samples/python/quickstart.py b/samples/python/quickstart.py index 300d9b2..f1a4d25 100644 --- a/samples/python/quickstart.py +++ b/samples/python/quickstart.py @@ -1,30 +1,30 @@ import os + import requests from dotenv import load_dotenv load_dotenv() -BASE_URL = os.getenv('PLATFORM_BASE_URL', 'https://api.gonitro.dev') -CLIENT_ID = os.getenv('PLATFORM_CLIENT_ID') -CLIENT_SECRET = os.getenv('PLATFORM_CLIENT_SECRET') +BASE_URL = os.getenv("PLATFORM_BASE_URL", "https://api.gonitro.dev") +CLIENT_ID = os.getenv("PLATFORM_CLIENT_ID") +CLIENT_SECRET = os.getenv("PLATFORM_CLIENT_SECRET") + -def get_access_token(): +def get_access_token() -> str: """Get OAuth2 access token using client credentials""" url = f"{BASE_URL}/oauth/token" - data = { - "clientID": CLIENT_ID, - "clientSecret": CLIENT_SECRET - } - + data = {"clientID": CLIENT_ID, "clientSecret": CLIENT_SECRET} + response = requests.post(url, json=data) response.raise_for_status() - return response.json()['accessToken'] + return response.json()["accessToken"] -def test_connection(token): + +def test_connection(token: str) -> bool | None: """Test API connection with a simple request""" url = f"{BASE_URL}/jobs/test-job-id/status" headers = {"Authorization": f"Bearer {token}"} - + try: response = requests.get(url, headers=headers) # 404 is expected for non-existent job, but proves auth works @@ -32,14 +32,15 @@ def test_connection(token): print("✅ Authentication successful (404 expected for test job ID)") return True response.raise_for_status() - return True + return True # noqa: TRY300 except requests.exceptions.HTTPError as e: if e.response.status_code == 404: print("✅ Authentication successful (404 expected for test job ID)") return True raise -def main(): + +def main() -> None: if not CLIENT_ID or not CLIENT_SECRET: print("❌ Missing credentials!") print("To get your credentials:") @@ -51,21 +52,22 @@ def main(): print(" export PLATFORM_CLIENT_ID=") print(" export PLATFORM_CLIENT_SECRET=") return - + try: print("🔐 Getting access token...") token = get_access_token() print("✅ Token obtained successfully") - + print("🧪 Testing API connection...") test_connection(token) print("✅ API connection successful") - + print("\n🎉 Setup complete! You can now use the Platform API.") print("📖 See https://developers.gonitro.com/docs for API documentation") - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f"❌ Error: {e}") + if __name__ == "__main__": main() diff --git a/samples/python/redact_by_keyword.py b/samples/python/redact_by_keyword.py index 32fb189..f8bec9e 100644 --- a/samples/python/redact_by_keyword.py +++ b/samples/python/redact_by_keyword.py @@ -4,17 +4,17 @@ =========================== The script exemplifies a typical workflow for targeted content redaction. -As a compliance officer, you need to redact specific -sensitive terms from documents before external sharing or public disclosure. -Whether removing client names, project codenames, financial figures, or -proprietary terminology, manually searching through pages and applying redactions +As a compliance officer, you need to redact specific +sensitive terms from documents before external sharing or public disclosure. +Whether removing client names, project codenames, financial figures, or +proprietary terminology, manually searching through pages and applying redactions is tedious and risks missing instances, potentially exposing confidential information. -This workflow automates keyword-based redaction. The script searches the entire -PDF document for all specified keywords and phrases, identifies their exact -locations across all pages, then automatically applies permanent redactions to -remove them. Multiple keywords can be processed in a single pass, ensuring -comprehensive coverage. The result is a thoroughly redacted document ready for +This workflow automates keyword-based redaction. The script searches the entire +PDF document for all specified keywords and phrases, identifies their exact +locations across all pages, then automatically applies permanent redactions to +remove them. Multiple keywords can be processed in a single pass, ensuring +comprehensive coverage. The result is a thoroughly redacted document ready for safe distribution. KEYWORD REDACTION FEATURES: @@ -35,81 +35,82 @@ import sys from pathlib import Path + from api.platform_api import PlatformAPIClient -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) < 4: - print("Usage: python redact_by_keyword.py [keyword2 ...]") - print("Example: python redact_by_keyword.py document.pdf redacted.pdf 'confidential' 'secret'") + print( + "Usage: python redact_by_keyword.py [keyword2 ...]" + ) + print( + "Example: python redact_by_keyword.py document.pdf redacted.pdf 'confidential' 'secret'" + ) sys.exit(1) - + # Get file paths and keywords from arguments input_path = Path(sys.argv[1]) output_path = Path(sys.argv[2]) keywords = sys.argv[3:] - + # Validate input file exists if not input_path.exists(): print(f"❌ Error: Input file not found: {input_path}") sys.exit(1) - + # Validate input is a PDF - if input_path.suffix.lower() != '.pdf': - print(f"❌ Error: Input must be a PDF file") + if input_path.suffix.lower() != ".pdf": + print("❌ Error: Input must be a PDF file") sys.exit(1) - + # Create output directory if needed output_path.parent.mkdir(parents=True, exist_ok=True) - + # Initialize API client (loads credentials from .env) client = PlatformAPIClient() - + try: # Step 1: Search for keywords in document print(f"🔍 Searching for {len(keywords)} keyword(s) in {input_path.name}...") print(f" Keywords: {', '.join(repr(k) for k in keywords)}") - + bbox_data = client.find_text_boxes(input_path, keywords) - + # Extract text box locations from response - text_boxes = bbox_data.get('result', {}).get('textBoxes', []) - + text_boxes = bbox_data.get("result", {}).get("textBoxes", []) + if not text_boxes: - print("ℹ️ No keyword matches found - copying original file") + print("ℹ️ No keyword matches found - copying original file") # noqa: RUF001 # Copy original file to output if no keywords found output_path.write_bytes(input_path.read_bytes()) print(f"✅ Saved: {output_path.name}") print(f"📂 Output: {output_path.absolute()}") return - + print(f"🎯 Found {len(text_boxes)} keyword instance(s) to redact") - + # Step 2: Prepare redaction coordinates print("🔒 Applying redactions...") redactions = [ - { - "pageIndex": box["pageIndex"], - "boundingBox": box["boundingBox"] - } - for box in text_boxes + {"pageIndex": box["pageIndex"], "boundingBox": box["boundingBox"]} for box in text_boxes ] - + # Step 3: Apply redactions to document redacted_pdf = client.redact(input_path, redactions) - + # Save redacted PDF output_path.write_bytes(redacted_pdf) - + # Display success message - print(f"✅ Redaction successful!") + print("✅ Redaction successful!") print(f"🔒 Redacted: {len(text_boxes)} instance(s)") print(f"📄 Input: {input_path.name}") print(f"📄 Output: {output_path.name}") print(f"📂 Saved to: {output_path.absolute()}") - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f"❌ Redaction FAILED: {e}") sys.exit(1) diff --git a/samples/python/requirements.txt b/samples/python/requirements.txt deleted file mode 100644 index da9bf17..0000000 --- a/samples/python/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests>=2.31.0 -python-dotenv>=1.0.0 -reportlab>=4.0.0 diff --git a/samples/python/smart_redact_pii.py b/samples/python/smart_redact_pii.py index 83fd1fd..41c012e 100644 --- a/samples/python/smart_redact_pii.py +++ b/samples/python/smart_redact_pii.py @@ -4,17 +4,17 @@ ====================== The script exemplifies a typical workflow for protecting sensitive customer information. -As a compliance officer, it's essential to review and redact -personally identifiable information (PII) from documents before sharing them with -third parties, storing them in public systems, or using them for analysis. Manual -redaction is time-consuming and error-prone, potentially missing sensitive data like +As a compliance officer, it's essential to review and redact +personally identifiable information (PII) from documents before sharing them with +third parties, storing them in public systems, or using them for analysis. Manual +redaction is time-consuming and error-prone, potentially missing sensitive data like social security numbers, phone numbers, addresses, or email addresses. -This workflow automates compliant document redaction. The script processes each PDF -file individually - for every document in the input folder, it uses AI-powered PII -detection to identify all instances of sensitive information across all pages, then -automatically applies redactions to permanently remove this data. Each processed file -is saved to the output folder with all PII securely redacted, ready for safe sharing +This workflow automates compliant document redaction. The script processes each PDF +file individually - for every document in the input folder, it uses AI-powered PII +detection to identify all instances of sensitive information across all pages, then +automatically applies redactions to permanently remove this data. Each processed file +is saved to the output folder with all PII securely redacted, ready for safe sharing or archival. PRIVACY COMPLIANCE STANDARDS: @@ -31,82 +31,80 @@ import sys from pathlib import Path + from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup -def main(): +def main() -> None: # Check command-line arguments if len(sys.argv) != 3: print("Usage: python smart_redact_pii.py ") sys.exit(1) - + # Get folder paths from arguments input_folder = Path(sys.argv[1]) output_folder = Path(sys.argv[2]) - + # Validate and setup (only process PDF files) - files = validate_and_setup(input_folder, output_folder, file_patterns=['*.pdf']) + files = validate_and_setup(input_folder, output_folder, file_patterns=["*.pdf"]) print(f"📋 Found {len(files)} PDF document(s) to process") - - # Initialize API client + + # Initialize API client client = PlatformAPIClient() - + # Process each document success_count = 0 failed_count = 0 total_pii_count = 0 - + for i, pdf_file in enumerate(files, 1): print(f"[{i}/{len(files)}] Processing: {pdf_file.name}") - + try: # Step 1: Detect PII in the document print(" 🔍 Detecting PII...") pii_data = client.detect_pii(pdf_file) - + # Extract PII bounding boxes from response - pii_boxes = pii_data.get('result', {}).get('PIIBoxes', []) - + pii_boxes = pii_data.get("result", {}).get("PIIBoxes", []) + if not pii_boxes: - print(" ℹ️ No PII detected - copying original file") - + print(" ℹ️ No PII detected - copying original file") # noqa: RUF001 + # Copy original file to output if no PII found output_file = output_folder / pdf_file.name output_file.write_bytes(pdf_file.read_bytes()) - + print(f" ✅ Saved: {output_file.name}") - + success_count += 1 continue - + print(f" 🎯 Found {len(pii_boxes)} PII instance(s)") total_pii_count += len(pii_boxes) - + # Step 2: Prepare redaction coordinates print(" 🔒 Applying redactions...") redactions = [ - { - "pageIndex": box["pageIndex"], - "boundingBox": box["boundingBox"] - } + {"pageIndex": box["pageIndex"], "boundingBox": box["boundingBox"]} for box in pii_boxes ] - + # Step 3: Apply redactions to document redacted_pdf = client.redact(pdf_file, redactions) - + # Save redacted PDF output_file = output_folder / pdf_file.name output_file.write_bytes(redacted_pdf) - + print(f" ✅ Redacted: {output_file.name}") success_count += 1 - - except Exception as e: + + except Exception as e: # noqa: BLE001 print(f" ❌ FAILED: {e}") failed_count += 1 - + # Display summary print("=" * 60) print(f"✅ {success_count} document(s) processed") diff --git a/samples/python/uv.lock b/samples/python/uv.lock new file mode 100644 index 0000000..4405526 --- /dev/null +++ b/samples/python/uv.lock @@ -0,0 +1,289 @@ +version = 1 +requires-python = ">=3.14" + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 }, +] + +[[package]] +name = "astroid" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354 }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "isort" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672 }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, +] + +[[package]] +name = "nitro-platform-samples" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "python-dotenv" }, + { name = "reportlab" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pyenchant" }, + { name = "pylint" }, + { name = "pyright" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438 }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531 }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554 }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812 }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689 }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186 }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308 }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222 }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657 }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482 }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416 }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584 }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621 }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916 }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836 }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092 }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158 }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882 }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001 }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146 }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344 }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864 }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911 }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045 }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282 }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630 }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731 }, +] + +[[package]] +name = "pyenchant" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ad/64925c937e41be75c7067c85757b3d45b148e9111187b37693269f583156/pyenchant-3.3.0.tar.gz", hash = "sha256:825288246b5debc9436f91967650974ef0d5636458502619e322c476f1283891", size = 60696 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/b0/35926bad6885fb7bc24aa7e1b45e6d86540c6c57ee4abc4fed1ef58d4ec0/pyenchant-3.3.0-py3-none-any.whl", hash = "sha256:3da00b1d01314d85aac733bb997415d7a3e875666dc81735ddcf320aa36b7a70", size = 58363 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/1d7b8ad86c2a841d940df7b965fa727e052b95d539e4c563da685c25d0d2/pyenchant-3.3.0-py3-none-win32.whl", hash = "sha256:1d55e075645a6edbb3c590fb42f9e02b4d455e4affe28a2227d5cb6d4868e626", size = 37787278 }, + { url = "https://files.pythonhosted.org/packages/ad/ae/5624803b62ecb0a20248f0d28ed3f78c78746a032582a016d4b2890c7899/pyenchant-3.3.0-py3-none-win_amd64.whl", hash = "sha256:04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49", size = 37427576 }, +] + +[[package]] +name = "pylint" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425 }, +] + +[[package]] +name = "pyright" +version = "1.1.407" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + +[[package]] +name = "reportlab" +version = "4.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] From 9d800105f267c3055c60bfc044bfb2ce559ce7b4 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 23 Dec 2025 16:27:06 +0000 Subject: [PATCH 12/19] fix request calls to use httpx --- samples/python/api/platform_api.py | 14 +- samples/python/api/sign_api.py | 16 +- .../python/helper_functions/sign_helpers.py | 6 +- samples/python/quickstart.py | 8 +- samples/python/uv.lock | 184 ++++++++++-------- 5 files changed, 123 insertions(+), 105 deletions(-) diff --git a/samples/python/api/platform_api.py b/samples/python/api/platform_api.py index 0cd562d..77a3517 100644 --- a/samples/python/api/platform_api.py +++ b/samples/python/api/platform_api.py @@ -1,6 +1,8 @@ #!/usr/bin/env python """Platform API client for Nitro Platform integrations.""" +from __future__ import annotations + import json import mimetypes import os @@ -8,7 +10,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal -import requests +import httpx from dotenv import load_dotenv if TYPE_CHECKING: @@ -34,7 +36,7 @@ def _get_token(self) -> str: if self._token and time.time() < self._token_expiry: return self._token - response = requests.post( + response = httpx.post( f"{self.base_url}/oauth/token", json={"clientID": self.client_id, "clientSecret": self.client_secret}, ) @@ -61,7 +63,7 @@ def _request( files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} data = {"method": method, "params": json.dumps(params or {})} - response = requests.post( + response = httpx.post( f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data ) @@ -85,7 +87,7 @@ def _request_bytes( files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} data = {"method": method, "params": json.dumps(params or {})} - response = requests.post( + response = httpx.post( f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data ) @@ -94,7 +96,7 @@ def _request_bytes( # Download from S3 URL download_url = result["result"]["file"]["URL"] - download_response = requests.get(download_url) + download_response = httpx.get(download_url) download_response.raise_for_status() return download_response.content @@ -155,7 +157,7 @@ def merge(self, file_paths: list[Path]) -> bytes: files = [("file", f) for f in opened_files] data = {"method": "merge", "params": "{}"} - response = requests.post( + response = httpx.post( f"{self.base_url}/transformations", headers=headers, files=files, data=data ) response.raise_for_status() diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py index 5f7fa9c..8a52547 100644 --- a/samples/python/api/sign_api.py +++ b/samples/python/api/sign_api.py @@ -1,13 +1,15 @@ #!/usr/bin/env python """Sign API client for Nitro Sign integrations (eSignature operations).""" +from __future__ import annotations + import json import os import time from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any -import requests +import httpx from dotenv import load_dotenv if TYPE_CHECKING: @@ -33,7 +35,7 @@ def _get_token(self) -> str: if self._token and time.time() < self._token_expiry: return self._token - response = requests.post( + response = httpx.post( f"{self.base_url}/oauth/token", json={"clientID": self.client_id, "clientSecret": self.client_secret}, ) @@ -53,7 +55,7 @@ def _request( """Make authenticated API request returning JSON.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - response = requests.request( + response = httpx.request( method=method, url=f"{self.base_url}{endpoint}", headers=headers, @@ -63,7 +65,7 @@ def _request( try: response.raise_for_status() - except requests.exceptions.HTTPError: + except httpx.HTTPError: # Try to get error details from response try: error_detail = response.json() @@ -80,7 +82,7 @@ def _request_bytes( """Make authenticated API request returning binary data.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - response = requests.request( + response = httpx.request( method=method, url=f"{self.base_url}{endpoint}", headers=headers, params=params ) @@ -150,7 +152,7 @@ def delete_envelope(self, envelope_id: str) -> None: envelope_id: UUID of the envelope """ headers = {"Authorization": f"Bearer {self._get_token()}"} - response = requests.delete(f"{self.base_url}/sign/envelopes/{envelope_id}", headers=headers) + response = httpx.delete(f"{self.base_url}/sign/envelopes/{envelope_id}", headers=headers) response.raise_for_status() # ========== Document Management ========== @@ -186,7 +188,7 @@ def create_document( headers = {"Authorization": f"Bearer {self._get_token()}"} - response = requests.post( + response = httpx.post( f"{self.base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files ) diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py index 2540876..48035ed 100644 --- a/samples/python/helper_functions/sign_helpers.py +++ b/samples/python/helper_functions/sign_helpers.py @@ -2,6 +2,8 @@ Sign API helper utilities for envelope operations. """ +from __future__ import annotations + import csv import json import time @@ -140,9 +142,9 @@ def create_signature_envelope( headers = {"Authorization": f"Bearer {sign_client._get_token()}"} - import requests + import httpx - response = requests.post( # noqa: S113 + response = httpx.post( f"{sign_client.base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files, diff --git a/samples/python/quickstart.py b/samples/python/quickstart.py index f1a4d25..06d6847 100644 --- a/samples/python/quickstart.py +++ b/samples/python/quickstart.py @@ -1,6 +1,6 @@ import os -import requests +import httpx from dotenv import load_dotenv load_dotenv() @@ -15,7 +15,7 @@ def get_access_token() -> str: url = f"{BASE_URL}/oauth/token" data = {"clientID": CLIENT_ID, "clientSecret": CLIENT_SECRET} - response = requests.post(url, json=data) + response = httpx.post(url, json=data) response.raise_for_status() return response.json()["accessToken"] @@ -26,14 +26,14 @@ def test_connection(token: str) -> bool | None: headers = {"Authorization": f"Bearer {token}"} try: - response = requests.get(url, headers=headers) + response = httpx.get(url, headers=headers) # 404 is expected for non-existent job, but proves auth works if response.status_code == 404: print("✅ Authentication successful (404 expected for test job ID)") return True response.raise_for_status() return True # noqa: TRY300 - except requests.exceptions.HTTPError as e: + except httpx.HTTPError as e: if e.response.status_code == 404: print("✅ Authentication successful (404 expected for test job ID)") return True diff --git a/samples/python/uv.lock b/samples/python/uv.lock index 4405526..84878b1 100644 --- a/samples/python/uv.lock +++ b/samples/python/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 2 requires-python = ">=3.14" [[package]] @@ -8,79 +9,79 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266 } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362 }, + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, ] [[package]] name = "astroid" version = "4.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714, upload-time = "2025-11-09T21:21:18.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354 }, + { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354, upload-time = "2025-11-09T21:21:16.54Z" }, ] [[package]] name = "certifi" version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "dill" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -91,9 +92,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] @@ -106,36 +107,36 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "isort" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049 } +sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672 }, + { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] @@ -155,66 +156,77 @@ dev = [ { name = "pyright" }, ] +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.0" }, + { name = "pyenchant", marker = "extra == 'dev'", specifier = ">=3.3.0" }, + { name = "pylint", marker = "extra == 'dev'", specifier = ">=4.0.4" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.407" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "reportlab", specifier = ">=4.0.0" }, +] +provides-extras = ["dev"] + [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611 } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438 }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "pillow" version = "12.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531 }, - { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554 }, - { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812 }, - { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689 }, - { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186 }, - { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308 }, - { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222 }, - { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657 }, - { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482 }, - { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416 }, - { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584 }, - { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621 }, - { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916 }, - { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836 }, - { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092 }, - { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158 }, - { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882 }, - { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001 }, - { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146 }, - { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344 }, - { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864 }, - { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911 }, - { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045 }, - { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282 }, - { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630 }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, ] [[package]] name = "platformdirs" version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731 }, + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] [[package]] name = "pyenchant" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/ad/64925c937e41be75c7067c85757b3d45b148e9111187b37693269f583156/pyenchant-3.3.0.tar.gz", hash = "sha256:825288246b5debc9436f91967650974ef0d5636458502619e322c476f1283891", size = 60696 } +sdist = { url = "https://files.pythonhosted.org/packages/36/ad/64925c937e41be75c7067c85757b3d45b148e9111187b37693269f583156/pyenchant-3.3.0.tar.gz", hash = "sha256:825288246b5debc9436f91967650974ef0d5636458502619e322c476f1283891", size = 60696, upload-time = "2025-09-14T16:23:12.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/b0/35926bad6885fb7bc24aa7e1b45e6d86540c6c57ee4abc4fed1ef58d4ec0/pyenchant-3.3.0-py3-none-any.whl", hash = "sha256:3da00b1d01314d85aac733bb997415d7a3e875666dc81735ddcf320aa36b7a70", size = 58363 }, - { url = "https://files.pythonhosted.org/packages/d6/7f/1d7b8ad86c2a841d940df7b965fa727e052b95d539e4c563da685c25d0d2/pyenchant-3.3.0-py3-none-win32.whl", hash = "sha256:1d55e075645a6edbb3c590fb42f9e02b4d455e4affe28a2227d5cb6d4868e626", size = 37787278 }, - { url = "https://files.pythonhosted.org/packages/ad/ae/5624803b62ecb0a20248f0d28ed3f78c78746a032582a016d4b2890c7899/pyenchant-3.3.0-py3-none-win_amd64.whl", hash = "sha256:04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49", size = 37427576 }, + { url = "https://files.pythonhosted.org/packages/38/b0/35926bad6885fb7bc24aa7e1b45e6d86540c6c57ee4abc4fed1ef58d4ec0/pyenchant-3.3.0-py3-none-any.whl", hash = "sha256:3da00b1d01314d85aac733bb997415d7a3e875666dc81735ddcf320aa36b7a70", size = 58363, upload-time = "2025-09-14T16:23:04.297Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7f/1d7b8ad86c2a841d940df7b965fa727e052b95d539e4c563da685c25d0d2/pyenchant-3.3.0-py3-none-win32.whl", hash = "sha256:1d55e075645a6edbb3c590fb42f9e02b4d455e4affe28a2227d5cb6d4868e626", size = 37787278, upload-time = "2025-09-14T16:23:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ae/5624803b62ecb0a20248f0d28ed3f78c78746a032582a016d4b2890c7899/pyenchant-3.3.0-py3-none-win_amd64.whl", hash = "sha256:04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49", size = 37427576, upload-time = "2025-09-14T16:23:09.574Z" }, ] [[package]] @@ -230,9 +242,9 @@ dependencies = [ { name = "platformdirs" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425 }, + { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, ] [[package]] @@ -243,18 +255,18 @@ dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008 }, + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, ] [[package]] name = "python-dotenv" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] [[package]] @@ -265,25 +277,25 @@ dependencies = [ { name = "charset-normalizer" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000, upload-time = "2025-12-21T11:50:11.336Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263 }, + { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263, upload-time = "2025-12-21T11:50:08.93Z" }, ] [[package]] name = "tomlkit" version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] From be0cec057de132aaf82be6e5264fab13990c4103 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 30 Dec 2025 10:11:16 +0000 Subject: [PATCH 13/19] fix pylint errors --- samples/python/README.md | 174 ++++++++++++++++-- samples/python/api/__init__.py | 7 + samples/python/api/base_client.py | 51 +++++ samples/python/api/platform_api.py | 42 ++--- samples/python/api/sign_api.py | 36 +--- samples/python/batch_process.py | 1 + samples/python/bulk_password_protect.py | 1 + samples/python/convert_cli.py | 1 + samples/python/employee_policy_onboarding.py | 164 +++++++++++------ samples/python/extract_data.py | 3 +- .../helper_functions/document_helpers.py | 2 +- .../python/helper_functions/sign_helpers.py | 110 ++++++----- .../python/prepare_pdf_for_distribution.py | 5 +- samples/python/quickstart.py | 5 +- samples/python/redact_by_keyword.py | 1 + samples/python/smart_redact_pii.py | 1 + 16 files changed, 419 insertions(+), 185 deletions(-) create mode 100644 samples/python/api/__init__.py create mode 100644 samples/python/api/base_client.py diff --git a/samples/python/README.md b/samples/python/README.md index 1c49dad..afa74fe 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -4,46 +4,98 @@ Python examples for integrating with the Nitro Platform API. ## Setup -1. Install dependencies: +### Option 1: Using uv (Recommended) + +This project uses [uv](https://docs.astral.sh/uv/) for fast, reliable Python package management. + +1. Install uv if you haven't already: + ```bash + curl -LsSf https://astral.sh/uv/install.sh | sh + ``` + +2. Sync dependencies: ```bash - pip install -r requirements.txt + cd samples/python + uv sync ``` -2. Copy and configure environment variables: +3. Copy and configure environment variables: ```bash cp .env.example .env # Edit .env with your credentials ``` -3. Run the quickstart example: +4. Run the quickstart example: ```bash python quickstart.py ``` -## API Clients +### Option 2: Using pip + +1. Create and activate a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` -- `api/platform_api.py` - Platform API client (conversions, extractions, transformations) -- `api/sign_api.py` - Sign API client (eSignature/envelopes) - **NEW!** +2. Install dependencies: + ```bash + pip install -e . # Installs from pyproject.toml + ``` -See [SIGN_API.md](SIGN_API.md) for detailed Sign API documentation. +3. Copy and configure environment variables: + ```bash + cp .env.example .env + # Edit .env with your credentials + ``` + +4. Run the quickstart example: + ```bash + python quickstart.py + ``` + +## Architecture + +### API Client Structure + +The Python SDK uses a clean, object-oriented architecture: + +``` +api/ +├── __init__.py # Package exports +├── base_client.py # BaseOAuthClient - Shared OAuth2 authentication +├── platform_api.py # PlatformAPIClient - Document operations +└── sign_api.py # SignAPIClient - eSignature operations +``` + +**BaseOAuthClient**: Base class providing OAuth2 authentication for all API clients +- Automatic token management and refresh +- Public `get_token()` method for accessing authentication tokens +- Shared by both Platform and Sign API clients + +**PlatformAPIClient**: Client for document conversions, extractions, and transformations +- Inherits authentication from BaseOAuthClient +- Methods: `convert()`, `extract_text()`, `detect_pii()`, `redact()`, `compress()`, etc. + +**SignAPIClient**: Client for eSignature/envelope operations +- Inherits authentication from BaseOAuthClient +- Methods: `create_envelope()`, `create_participant()`, `send_envelope()`, etc. +- See [SIGN_API.md](SIGN_API.md) for detailed documentation ## CLI Tools ### Platform API Tools -- `platform_api.py` - Main API client library (legacy location) - `quickstart.py` - Authentication test - `convert_cli.py` - Document conversion -- `extract_data.py` - Extract forms and tables +- `extract_data.py` - Extract forms and tables from PDFs - `smart_redact_pii.py` - Auto-detect and redact PII - `redact_by_keyword.py` - Redact specific keywords - `batch_process.py` - Batch convert documents - `bulk_password_protect.py` - Password protect multiple PDFs +- `prepare_pdf_for_distribution.py` - Prepare PDFs for external distribution (convert, compress, remove metadata) ### Sign API Tools (eSignature) -- `send_policies_to_employees.py` - Send multiple policy documents to multiple employees for signature - -### Complete Workflow Examples -- `prepare_pdf_for_distribution.py` - Prepare PDFs for external distribution (convert, compress, remove metadata) +- `employee_policy_onboarding.py` - Complete HR workflow: send policy documents to employees for signature ## Usage Examples @@ -90,12 +142,19 @@ python bulk_password_protect.py ./input ./output "MyPassword123" ## Using the API Client +### Platform API Client + ```python from pathlib import Path from api.platform_api import PlatformAPIClient +# Initialize client (loads credentials from .env) client = PlatformAPIClient() +# Get authentication token (if needed) +token = client.get_token() +print(f"Access token: {token[:20]}...") + # Convert document converted = client.convert(Path("input.docx"), "pdf") Path("output.pdf").write_bytes(converted) @@ -111,6 +170,93 @@ redactions = [{"pageIndex": 0, "boundingBox": {...}}] redacted = client.redact(Path("document.pdf"), redactions) ``` +### Sign API Client + +```python +from pathlib import Path +from api.sign_api import SignAPIClient + +# Initialize client (loads credentials from .env) +sign_client = SignAPIClient() + +# Create an envelope +envelope_data = { + "name": "Contract Signature", + "mode": "parallel", + "notification": { + "subject": "Please sign the contract", + "body": "Review and sign the attached document." + } +} +envelope = sign_client.create_envelope(envelope_data) +envelope_id = envelope["ID"] + +# Add participant +participant_data = { + "email": "signer@example.com", + "role": "signer", + "name": "John Doe" +} +participant = sign_client.create_participant(envelope_id, participant_data) + +# Send envelope +sign_client.send_envelope(envelope_id) + +print(f"Envelope {envelope_id} sent successfully!") +``` + +### Shared Authentication + +Both clients inherit from `BaseOAuthClient`, so they share the same authentication mechanism: + +```python +from api.platform_api import PlatformAPIClient +from api.sign_api import SignAPIClient + +# Both clients use the same credentials from .env +platform_client = PlatformAPIClient() +sign_client = SignAPIClient() + +# Both can access tokens via the public API +platform_token = platform_client.get_token() +sign_token = sign_client.get_token() +``` + +## Code Quality + +This project maintains high code quality standards: + +- **Type Checking**: Strict type checking with Pyright (0 errors) +- **Linting**: Modern Python patterns with Ruff +- **Code Quality**: Pylint score of 9.81/10 +- **Python Version**: Requires Python 3.14+ + +### Running Linters + +```bash +# Type checking +pyright + +# Modern Python patterns +uv run --with ruff ruff check . + +# Code quality analysis +uv run pylint *.py api/*.py helper_functions/*.py +``` + +## Testing + +A comprehensive test suite is available in `TEST_SUITE.txt` with commands for testing all scripts and functionality. Run tests with: + +```bash +# Individual script tests +python quickstart.py +python convert_cli.py ../../test_files/test-batch/Analysis.docx /tmp/output.pdf pdf + +# Or use the automated test script (see TEST_SUITE.txt) +./test_all.sh +``` + ## Sample Files Sample files for testing are available in the `test_files/` folder at the repository root. diff --git a/samples/python/api/__init__.py b/samples/python/api/__init__.py new file mode 100644 index 0000000..d88fe23 --- /dev/null +++ b/samples/python/api/__init__.py @@ -0,0 +1,7 @@ +"""API clients for Nitro Platform integrations.""" + +from .base_client import BaseOAuthClient +from .platform_api import PlatformAPIClient +from .sign_api import SignAPIClient + +__all__ = ["BaseOAuthClient", "PlatformAPIClient", "SignAPIClient"] diff --git a/samples/python/api/base_client.py b/samples/python/api/base_client.py new file mode 100644 index 0000000..1e18df5 --- /dev/null +++ b/samples/python/api/base_client.py @@ -0,0 +1,51 @@ +"""Base client for API authentication with OAuth2.""" + +from __future__ import annotations + +import os +import time +from dataclasses import dataclass, field + +import httpx +from dotenv import load_dotenv + +load_dotenv() + + +@dataclass +class BaseOAuthClient: + """Base class for API clients with OAuth2 authentication.""" + + base_url: str = field( + default_factory=lambda: os.getenv("PLATFORM_BASE_URL") or "https://api.gonitro.dev" + ) + client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID") or "") + client_secret: str = field( + default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET") or "" + ) + _token: str | None = field(default=None, init=False) + _token_expiry: float = field(default=0, init=False) + + def _get_token(self) -> str: + """Get or refresh OAuth2 access token.""" + if self._token and time.time() < self._token_expiry: + return self._token + + response = httpx.post( + f"{self.base_url}/oauth/token", + json={"clientID": self.client_id, "clientSecret": self.client_secret}, + ) + response.raise_for_status() + data = response.json() + token: str = data["accessToken"] + self._token = token + self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 + return token + + def get_token(self) -> str: + """Public method to get authentication token. + + Returns: + OAuth2 access token for API authentication. + """ + return self._get_token() diff --git a/samples/python/api/platform_api.py b/samples/python/api/platform_api.py index 77a3517..f2da1e1 100644 --- a/samples/python/api/platform_api.py +++ b/samples/python/api/platform_api.py @@ -5,47 +5,21 @@ import json import mimetypes -import os -import time -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal import httpx -from dotenv import load_dotenv + +from .base_client import BaseOAuthClient if TYPE_CHECKING: from pathlib import Path -load_dotenv() - @dataclass -class PlatformAPIClient: +class PlatformAPIClient(BaseOAuthClient): """Synchronous client for Nitro Platform API operations.""" - base_url: str = field( - default_factory=lambda: os.getenv("PLATFORM_BASE_URL", "https://api.gonitro.dev") - ) - client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID")) - client_secret: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET")) - _token: str | None = field(default=None, init=False) - _token_expiry: float = field(default=0, init=False) - - def _get_token(self) -> str: - """Get or refresh OAuth2 access token.""" - if self._token and time.time() < self._token_expiry: - return self._token - - response = httpx.post( - f"{self.base_url}/oauth/token", - json={"clientID": self.client_id, "clientSecret": self.client_secret}, - ) - response.raise_for_status() - data = response.json() - self._token = data["accessToken"] - self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 - return self._token - def _request( self, endpoint: Literal["conversions", "extractions", "transformations"], @@ -128,7 +102,7 @@ def find_text_boxes(self, file_path: Path, texts: list[str]) -> dict[str, Any]: "extractions", "extract-text-bounding-boxes", file_path, {"texts": texts} ) - def redact(self, file_path: Path, redactions: list[dict]) -> bytes: + def redact(self, file_path: Path, redactions: list[dict[str, Any]]) -> bytes: """Redact specified bounding boxes.""" return self._request_bytes( "transformations", "redact", file_path, {"redactions": redactions} @@ -147,6 +121,12 @@ def compress(self, file_path: Path, level: int = 2) -> bytes: """Compress PDF (level 1-3).""" return self._request_bytes("transformations", "compress", file_path, {"level": level}) + def set_properties(self, file_path: Path, properties: dict[str, str]) -> bytes: + """Set or clear PDF metadata properties.""" + return self._request_bytes( + "transformations", "set-properties", file_path, properties + ) + def merge(self, file_paths: list[Path]) -> bytes: """Merge multiple PDFs.""" headers = {"Authorization": f"Bearer {self._get_token()}"} diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py index 8a52547..bd0585d 100644 --- a/samples/python/api/sign_api.py +++ b/samples/python/api/sign_api.py @@ -4,47 +4,21 @@ from __future__ import annotations import json -import os -import time -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import TYPE_CHECKING, Any import httpx -from dotenv import load_dotenv + +from .base_client import BaseOAuthClient if TYPE_CHECKING: from pathlib import Path -load_dotenv() - @dataclass -class SignAPIClient: +class SignAPIClient(BaseOAuthClient): """Synchronous client for Nitro Sign API operations (eSignature/envelopes).""" - base_url: str = field( - default_factory=lambda: os.getenv("PLATFORM_BASE_URL", "https://api.gonitro.dev") - ) - client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID")) - client_secret: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET")) - _token: str | None = field(default=None, init=False) - _token_expiry: float = field(default=0, init=False) - - def _get_token(self) -> str: - """Get or refresh OAuth2 access token.""" - if self._token and time.time() < self._token_expiry: - return self._token - - response = httpx.post( - f"{self.base_url}/oauth/token", - json={"clientID": self.client_id, "clientSecret": self.client_secret}, - ) - response.raise_for_status() - data = response.json() - self._token = data["accessToken"] - self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 - return self._token - def _request( self, method: str, @@ -103,7 +77,7 @@ def list_envelopes( Returns: Dict with 'items' (list of envelopes) and optional 'nextPage' (cursor token) """ - params = {} + params: dict[str, str] = {} if page_after: params["pageAfter"] = page_after elif page_before: diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index 7d4ebe8..37d8fd5 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -40,6 +40,7 @@ def main() -> None: + """Process multiple documents in batch, converting them to a specified format.""" # Check command-line arguments if len(sys.argv) < 4: print("Usage: python batch_process.py [pattern]") diff --git a/samples/python/bulk_password_protect.py b/samples/python/bulk_password_protect.py index a8c76b0..b3d7429 100644 --- a/samples/python/bulk_password_protect.py +++ b/samples/python/bulk_password_protect.py @@ -37,6 +37,7 @@ def main() -> None: + """Apply password protection to all PDF files in a directory.""" # Check command-line arguments if len(sys.argv) != 4: print("Usage: python bulk_password_protect.py ") diff --git a/samples/python/convert_cli.py b/samples/python/convert_cli.py index bd546b8..76c50f6 100644 --- a/samples/python/convert_cli.py +++ b/samples/python/convert_cli.py @@ -38,6 +38,7 @@ def main() -> None: + """Convert a document from one format to another using the Platform API.""" # Check command-line arguments if len(sys.argv) != 4: print("Usage: python convert_cli.py ") diff --git a/samples/python/employee_policy_onboarding.py b/samples/python/employee_policy_onboarding.py index c516f33..e18fccf 100644 --- a/samples/python/employee_policy_onboarding.py +++ b/samples/python/employee_policy_onboarding.py @@ -47,6 +47,7 @@ import sys from pathlib import Path +from typing import Any from api.sign_api import SignAPIClient from helper_functions.sign_helpers import ( @@ -65,17 +66,18 @@ class EnvelopeNotSignedError(Exception): """Raised when an envelope was not signed in time.""" -def main() -> None: - # Check command-line arguments - if len(sys.argv) != 3: - print("Usage: python employee_policy_onboarding.py ") - sys.exit(1) +def _validate_and_setup_inputs( + policies_folder: Path, employees_csv: Path +) -> tuple[Path, list[dict[str, str]], list[dict[str, Any]]]: + """Validate inputs and load data. - # Parse arguments - policies_folder = Path(sys.argv[1]) - employees_csv = Path(sys.argv[2]) - output_folder = Path("output") + Args: + policies_folder: Path to folder containing policy PDFs + employees_csv: Path to CSV file with employee data + Returns: + Tuple of (output_folder, employees, documents) + """ # Validate inputs if not policies_folder.exists() or not policies_folder.is_dir(): print(f"❌ Policies folder not found: {policies_folder}") @@ -86,6 +88,7 @@ def main() -> None: sys.exit(1) # Display header + output_folder = Path("output") print("=" * 60) print("📝 SEND POLICIES TO EMPLOYEES") print("=" * 60) @@ -95,15 +98,94 @@ def main() -> None: print("=" * 60) print() - try: - # Load employees and documents - employees = load_employees_from_csv(employees_csv) - print(f"👥 Found {len(employees)} employee(s)\n") + # Load employees and documents + employees = load_employees_from_csv(employees_csv) + print(f"👥 Found {len(employees)} employee(s)\n") + + documents = load_policy_documents_from_folder(policies_folder) + + # Create output folder + output_folder.mkdir(parents=True, exist_ok=True) + + return output_folder, employees, documents + + +def _process_employee_onboarding( + sign_client: SignAPIClient, + employee: dict[str, str], + documents: list[dict[str, Any]], + output_folder: Path, + *, + employee_num: int, + total_employees: int, +) -> None: + """Process onboarding workflow for a single employee. + + Args: + sign_client: Sign API client instance + employee: Employee data dict with 'name' and 'email' + documents: List of policy documents to send + output_folder: Base output folder for signed documents + employee_num: Current employee number (for display) + total_employees: Total number of employees (for display) + + Raises: + EnvelopeNotSignedError: If envelope is not signed within timeout + Exception: For other errors during processing + """ + name = employee["name"] + email = employee["email"] + + print(f"\n[{employee_num}/{total_employees}] {name}") + + # Create employee-specific output folder + employee_folder = output_folder / create_employee_folder_name(name) + employee_folder.mkdir(parents=True, exist_ok=True) + + # Create envelope and upload documents + log_step("📝 Creating envelope...") + envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) + + # Add participant (signer) + log_step("👤 Adding signer...") + participant_id = sign_client.create_participant( + envelope_id, {"email": email, "role": "signer", "name": name} + )["ID"] + + # Add signature fields to all documents + log_step("✍️ Adding fields...") + add_signature_fields_to_documents(sign_client, envelope_id, document_ids, participant_id) + + # Send and monitor envelope + log_step("📤 Sending...") + status = send_and_monitor_envelope(sign_client, envelope_id, email, timeout_minutes=60) + + if status != "sealed": + raise EnvelopeNotSignedError(f"Envelope not signed: {status}") - documents = load_policy_documents_from_folder(policies_folder) + # Download signed documents + log_step("📥 Downloading...") + download_signed_document(sign_client, envelope_id, employee_folder, "signed-policies.zip") - # Create output folder - output_folder.mkdir(parents=True, exist_ok=True) + print(" ✅ Completed\n") + + +def main() -> None: + """Send company policy documents to employees for electronic signature via Sign API.""" + # Check command-line arguments + if len(sys.argv) != 3: + print("Usage: python employee_policy_onboarding.py ") + sys.exit(1) + + # Parse arguments + policies_folder = Path(sys.argv[1]) + employees_csv = Path(sys.argv[2]) + + try: + # Validate inputs and load data + output_folder, employees, documents = _validate_and_setup_inputs( + policies_folder, employees_csv + ) # Initialize Sign API client sign_client = SignAPIClient() @@ -117,51 +199,15 @@ def main() -> None: failed_count = 0 for i, employee in enumerate(employees, 1): - name = employee["name"] - email = employee["email"] - - print(f"\n[{i}/{len(employees)}] {name}") - try: - # Create employee-specific output folder - folder_name = create_employee_folder_name(name) - employee_folder = output_folder / folder_name - employee_folder.mkdir(parents=True, exist_ok=True) - - # Create envelope and upload documents - log_step("📝 Creating envelope...") - envelope_id, document_ids = create_signature_envelope( - sign_client, documents, name, email - ) - - # Add participant (signer) - log_step("👤 Adding signer...") - participant_data = {"email": email, "role": "signer", "name": name} - participant = sign_client.create_participant(envelope_id, participant_data) - participant_id = participant["ID"] - - # Add signature fields to all documents - log_step("✍️ Adding fields...") - add_signature_fields_to_documents( - sign_client, envelope_id, document_ids, participant_id + _process_employee_onboarding( + sign_client, + employee, + documents, + output_folder, + employee_num=i, + total_employees=len(employees), ) - - # Send and monitor envelope - log_step("📤 Sending...") - status = send_and_monitor_envelope( - sign_client, envelope_id, email, timeout_minutes=60 - ) - - if status != "sealed": - raise EnvelopeNotSignedError(f"Envelope not signed: {status}") # noqa: TRY301 - - # Download signed documents - log_step("📥 Downloading...") - download_signed_document( - sign_client, envelope_id, employee_folder, "signed-policies.zip" - ) - - print(" ✅ Completed\n") success_count += 1 except Exception as e: # noqa: BLE001 diff --git a/samples/python/extract_data.py b/samples/python/extract_data.py index 7ec7c35..8238966 100644 --- a/samples/python/extract_data.py +++ b/samples/python/extract_data.py @@ -42,6 +42,7 @@ def main() -> None: + """Extract structured data (forms or tables) from PDF documents.""" # Check command-line arguments if len(sys.argv) != 4: print("Usage: python extract_data.py ") @@ -95,7 +96,7 @@ def main() -> None: item_count = len(result.get("tables", [])) # Save extracted data as JSON - output_path.write_text(json.dumps(data, indent=2)) + output_path.write_text(json.dumps(data, indent=2), encoding="utf-8") # Display success message print("✅ Extraction successful!") diff --git a/samples/python/helper_functions/document_helpers.py b/samples/python/helper_functions/document_helpers.py index 6879a0e..952bf42 100644 --- a/samples/python/helper_functions/document_helpers.py +++ b/samples/python/helper_functions/document_helpers.py @@ -36,7 +36,7 @@ def validate_and_setup( output_folder.mkdir(parents=True, exist_ok=True) # Find all matching files - files = [] + files: list[Path] = [] for pattern in file_patterns: files.extend(input_folder.glob(pattern)) diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py index 48035ed..ece34d2 100644 --- a/samples/python/helper_functions/sign_helpers.py +++ b/samples/python/helper_functions/sign_helpers.py @@ -8,9 +8,11 @@ import json import time import zipfile -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path # noqa: TC003 -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +import httpx if TYPE_CHECKING: from api.sign_api import SignAPIClient @@ -28,7 +30,7 @@ def create_employee_folder_name(employee_name: str) -> str: return employee_name.lower().replace(" ", "-").replace(".", "") -def load_employees_from_csv(csv_path: Path) -> list[dict]: +def load_employees_from_csv(csv_path: Path) -> list[dict[str, str]]: """Load employee list from CSV file. Args: @@ -43,13 +45,14 @@ def load_employees_from_csv(csv_path: Path) -> list[dict]: if not csv_path.exists(): raise ValueError(f"CSV file not found: {csv_path}") - employees = [] + employees: list[dict[str, str]] = [] with csv_path.open(encoding="utf-8") as f: reader = csv.DictReader(f) # Validate CSV has required columns - if "name" not in reader.fieldnames or "email" not in reader.fieldnames: + fieldnames = reader.fieldnames + if not fieldnames or "name" not in fieldnames or "email" not in fieldnames: raise ValueError('CSV must have "name" and "email" columns') for row in reader: @@ -65,7 +68,7 @@ def load_employees_from_csv(csv_path: Path) -> list[dict]: return employees -def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: +def load_policy_documents_from_folder(policies_folder: Path) -> list[dict[str, Any]]: """Load all PDF files from the policies folder. Args: @@ -81,7 +84,7 @@ def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: print(f"📂 Found {len(policy_files)} policy document(s)\n") - documents = [] + documents: list[dict[str, Any]] = [] for pf in policy_files: with pf.open("rb") as f: binary_data = f.read() @@ -91,41 +94,20 @@ def load_policy_documents_from_folder(policies_folder: Path) -> list[dict]: return documents -def create_signature_envelope( - sign_client: SignAPIClient, - documents: list[dict], - employee_name: str, - _employee_email: str, -) -> tuple[str, list[str]]: - """Create envelope and upload documents. +def _upload_documents_to_envelope( + sign_client: SignAPIClient, envelope_id: str, documents: list[dict[str, Any]] +) -> list[str]: + """Upload documents to an existing envelope. Args: sign_client: Sign API client instance + envelope_id: ID of the envelope to upload to documents: List of document dicts with 'name', 'binary', 'path' - employee_name: Full name of employee - employee_email: Email address of employee Returns: - Tuple of (envelope_id, list of document_ids) + List of document IDs """ - # Create empty envelope - envelope_data = { - "name": f"Company Policies - {employee_name}", - "mode": "parallel", - "notification": { - "subject": "Please sign: Company Policies", - "body": ( - f"Hello {employee_name}, please review and sign the attached " - "company policy documents." - ), - }, - } - - envelope = sign_client.create_envelope(envelope_data) - envelope_id = envelope["ID"] - - # Upload documents to envelope - document_ids = [] + document_ids: list[str] = [] for doc in documents: doc_name = doc["name"] @@ -140,9 +122,8 @@ def create_signature_envelope( "payload": (doc_name, doc_binary, "application/pdf"), } - headers = {"Authorization": f"Bearer {sign_client._get_token()}"} - - import httpx + token = sign_client.get_token() + headers = {"Authorization": f"Bearer {token}"} response = httpx.post( f"{sign_client.base_url}/sign/envelopes/{envelope_id}/documents", @@ -156,6 +137,45 @@ def create_signature_envelope( document_id = document["ID"] document_ids.append(document_id) + return document_ids + + +def create_signature_envelope( + sign_client: SignAPIClient, + documents: list[dict[str, Any]], + employee_name: str, + _employee_email: str, +) -> tuple[str, list[str]]: + """Create envelope and upload documents. + + Args: + sign_client: Sign API client instance + documents: List of document dicts with 'name', 'binary', 'path' + employee_name: Full name of employee + employee_email: Email address of employee + + Returns: + Tuple of (envelope_id, list of document_ids) + """ + # Create empty envelope + envelope_data = { + "name": f"Company Policies - {employee_name}", + "mode": "parallel", + "notification": { + "subject": "Please sign: Company Policies", + "body": ( + f"Hello {employee_name}, please review and sign the attached " + "company policy documents." + ), + }, + } + + envelope = sign_client.create_envelope(envelope_data) + envelope_id = envelope["ID"] + + # Upload documents to envelope + document_ids = _upload_documents_to_envelope(sign_client, envelope_id, documents) + return envelope_id, document_ids @@ -174,24 +194,26 @@ def add_signature_fields_to_documents( participant_id: ID of the participant who will sign """ for doc_id in document_ids: - # Add signature field + # Add signature field (positioned bottom-left of page) + # Coordinates: [x, y, width, height] in points (72 points = 1 inch) + # Standard letter page: 612 x 792 points signature_field_data = { "participantID": participant_id, "type": "signature", "label": "Your Signature", "page": 1, - "boundingBox": [200, 300, 60, 40], + "boundingBox": [50, 100, 200, 50], # Bottom area, safe coordinates "required": True, } sign_client.create_field(envelope_id, doc_id, signature_field_data) - # Add date field + # Add date field (positioned to the right of signature) date_field_data = { "participantID": participant_id, "type": "date", "label": "Date Signed", "page": 1, - "boundingBox": [320, 650, 150, 50], + "boundingBox": [270, 100, 150, 50], # Next to signature, safe coordinates "required": True, "format": "MM/DD/YYYY", } @@ -216,7 +238,7 @@ def send_and_monitor_envelope( sign_client.send_for_signing(envelope_id) # Log send time and status - send_time = datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d %H:%M:%S") + send_time = datetime.now(tz=UTC).strftime("%Y-%m-%d %H:%M:%S") print(f" ✅ Sent at: {send_time}") print(f" 📧 Email sent to: {email}") print(f" 🔗 Envelope ID: {envelope_id}") @@ -232,7 +254,7 @@ def send_and_monitor_envelope( status = monitor_envelope(sign_client, envelope_id, timeout_minutes) # Log final status - completion_time = datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d %H:%M:%S") + completion_time = datetime.now(tz=UTC).strftime("%Y-%m-%d %H:%M:%S") print(f" 📊 Final status: {status}") print(f" 🕐 Completed at: {completion_time}") diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index f38aafc..b44e894 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -41,6 +41,7 @@ def main() -> None: + """Prepare documents for distribution by converting to PDF and removing metadata.""" # Check command-line arguments if len(sys.argv) != 3: print("Usage: python prepare_pdf_for_distribution.py ") @@ -82,9 +83,7 @@ def main() -> None: # Step 3: Remove metadata properties print(" 🔒 Removing metadata...") properties_to_clear = dict.fromkeys(PROPERTIES_TO_REMOVE, "") - clean_pdf = client._request_bytes( - "transformations", "set-properties", temp_pdf, properties_to_clear - ) + clean_pdf = client.set_properties(temp_pdf, properties_to_clear) # Save final PDF final_pdf = output_folder / f"{doc.stem}.pdf" diff --git a/samples/python/quickstart.py b/samples/python/quickstart.py index 06d6847..cbc34fc 100644 --- a/samples/python/quickstart.py +++ b/samples/python/quickstart.py @@ -1,3 +1,5 @@ +"""Quickstart script to test Nitro Platform API authentication and connection.""" + import os import httpx @@ -33,7 +35,7 @@ def test_connection(token: str) -> bool | None: return True response.raise_for_status() return True # noqa: TRY300 - except httpx.HTTPError as e: + except httpx.HTTPStatusError as e: if e.response.status_code == 404: print("✅ Authentication successful (404 expected for test job ID)") return True @@ -41,6 +43,7 @@ def test_connection(token: str) -> bool | None: def main() -> None: + """Test API authentication and connection.""" if not CLIENT_ID or not CLIENT_SECRET: print("❌ Missing credentials!") print("To get your credentials:") diff --git a/samples/python/redact_by_keyword.py b/samples/python/redact_by_keyword.py index f8bec9e..c2deacd 100644 --- a/samples/python/redact_by_keyword.py +++ b/samples/python/redact_by_keyword.py @@ -40,6 +40,7 @@ def main() -> None: + """Redact specific keywords from PDF documents using text search.""" # Check command-line arguments if len(sys.argv) < 4: print( diff --git a/samples/python/smart_redact_pii.py b/samples/python/smart_redact_pii.py index 41c012e..43679b4 100644 --- a/samples/python/smart_redact_pii.py +++ b/samples/python/smart_redact_pii.py @@ -37,6 +37,7 @@ def main() -> None: + """Automatically detect and redact PII (personally identifiable information) from PDFs.""" # Check command-line arguments if len(sys.argv) != 3: print("Usage: python smart_redact_pii.py ") From 8b88e63a9529c6bc0f22ad6d1284fda03e42b9ac Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 30 Dec 2025 10:20:37 +0000 Subject: [PATCH 14/19] updated documentation and taskfile --- samples/python/.gitignore | 6 ++- samples/python/.python-version | 1 + samples/python/README.md | 70 +++++++++++++++++++++++++++++----- samples/python/Taskfile.yml | 20 +++++----- 4 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 samples/python/.python-version diff --git a/samples/python/.gitignore b/samples/python/.gitignore index 56e8318..66f64d8 100644 --- a/samples/python/.gitignore +++ b/samples/python/.gitignore @@ -13,6 +13,10 @@ __pycache__/ .Python *.egg-info/ -# Virtual environment +# uv .venv/ +uv.lock + +# Ruff cache +.ruff_cache/ diff --git a/samples/python/.python-version b/samples/python/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/samples/python/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/samples/python/README.md b/samples/python/README.md index afa74fe..34d163e 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -51,9 +51,59 @@ This project uses [uv](https://docs.astral.sh/uv/) for fast, reliable Python pac 4. Run the quickstart example: ```bash - python quickstart.py + uv run python quickstart.py ``` +## Development + +### Running Scripts with uv + +All Python scripts should be run using `uv run` to ensure they use the correct dependencies: + +```bash +uv run python script.py +``` + +### Code Quality Tools + +This project uses multiple linting and type checking tools configured in `pyproject.toml`: + +#### Ruff (Fast Python linter) +```bash +# Check for linting issues +uvx ruff check + +# Auto-fix issues +uvx ruff check --fix + +# Format code +uvx ruff format +``` + +#### Pylint (Comprehensive linter) +```bash +# Check all Python files +uv run pylint *.py api/*.py helper_functions/*.py + +# Check specific file +uv run pylint convert_cli.py +``` + +#### Pyright (Type checker) +```bash +# Type check all files +uv run pyright + +# Type check specific file +uv run pyright convert_cli.py +``` + +#### Run All Quality Checks +```bash +# Run all three tools +uvx ruff check && uv run pylint *.py api/*.py helper_functions/*.py && uv run pyright +``` + ## Architecture ### API Client Structure @@ -101,43 +151,43 @@ api/ ### Authentication ```bash -python quickstart.py +uv run python quickstart.py ``` ### Convert Documents ```bash # Convert DOCX to PDF -python convert_cli.py input.docx output.pdf pdf +uv run python convert_cli.py input.docx output.pdf pdf # Convert PDF to DOCX -python convert_cli.py input.pdf output.docx docx +uv run python convert_cli.py input.pdf output.docx docx ``` ### Extract Data ```bash # Extract tables -python extract_data.py tables input.pdf output.json +uv run python extract_data.py tables input.pdf output.json # Extract forms -python extract_data.py forms input.pdf output.json +uv run python extract_data.py forms input.pdf output.json ``` ### Redact Content ```bash # Auto-detect and redact PII -python smart_redact_pii.py input.pdf output.pdf +uv run python smart_redact_pii.py input_folder output_folder # Redact specific keywords -python redact_by_keyword.py input.pdf output.pdf "confidential" "secret" +uv run python redact_by_keyword.py input_folder output_folder "confidential" "secret" ``` ### Batch Operations ```bash # Convert all DOCX files to PDF -python batch_process.py ./input ./output pdf "*.docx" +uv run python batch_process.py ./input ./output pdf "*.docx" # Password protect all PDFs -python bulk_password_protect.py ./input ./output "MyPassword123" +uv run python bulk_password_protect.py ./input ./output "MyPassword123" ``` ## Using the API Client diff --git a/samples/python/Taskfile.yml b/samples/python/Taskfile.yml index 01ce63d..01f4ec0 100644 --- a/samples/python/Taskfile.yml +++ b/samples/python/Taskfile.yml @@ -4,49 +4,49 @@ tasks: smart-redact: desc: "Smart redact PII from documents in a folder (e.g., task smart-redact INPUT_DIR=./documents OUTPUT_DIR=./redacted)" cmds: - - python smart_redact_pii.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" + - uv run python smart_redact_pii.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" requires: vars: [INPUT_DIR, OUTPUT_DIR] convert: desc: "Convert document from CLI (e.g., task convert INPUT=doc.docx OUTPUT=doc.pdf FORMAT=pdf)" cmds: - - python convert_cli.py {{.INPUT}} {{.OUTPUT}} {{.FORMAT}} + - uv run python convert_cli.py {{.INPUT}} {{.OUTPUT}} {{.FORMAT}} requires: vars: [INPUT, OUTPUT, FORMAT] batch: desc: "Batch process documents (e.g., task batch INPUT_DIR=./docs OUTPUT_DIR=./output FORMAT=pdf PATTERN='*.docx')" cmds: - - python batch_process.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" {{.FORMAT}} "{{.PATTERN | default "*"}}" + - uv run python batch_process.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" {{.FORMAT}} "{{.PATTERN | default "*"}}" requires: vars: [INPUT_DIR, OUTPUT_DIR, FORMAT] extract: desc: "Extract forms or tables from PDF (e.g., task extract MODE=forms INPUT=form.pdf OUTPUT=data.json)" cmds: - - python extract_data.py {{.MODE}} "{{.INPUT}}" "{{.OUTPUT}}" + - uv run python extract_data.py {{.MODE}} "{{.INPUT}}" "{{.OUTPUT}}" requires: vars: [MODE, INPUT, OUTPUT] password-protect: desc: "Bulk password protect PDFs (e.g., task password-protect INPUT_DIR=./pdfs OUTPUT_DIR=./protected PASSWORD=secret123)" cmds: - - python bulk_password_protect.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" "{{.PASSWORD}}" + - uv run python bulk_password_protect.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" "{{.PASSWORD}}" requires: vars: [INPUT_DIR, OUTPUT_DIR, PASSWORD] redact-keyword: desc: "Redact specific keywords from a PDF (e.g., task redact-keyword INPUT=doc.pdf OUTPUT=redacted.pdf KEYWORDS='confidential secret')" cmds: - - python redact_by_keyword.py {{.INPUT}} {{.OUTPUT}} {{.KEYWORDS}} + - uv run python redact_by_keyword.py {{.INPUT}} {{.OUTPUT}} {{.KEYWORDS}} requires: vars: [INPUT, OUTPUT, KEYWORDS] install: - desc: "Install Python dependencies" + desc: "Install Python dependencies using uv" cmds: - - pip install -r requirements.txt + - uv sync setup: desc: "Setup environment (copy .env.example to .env)" @@ -57,7 +57,7 @@ tasks: prepare-distribution: desc: "Prepare documents for external distribution (convert, compress, remove metadata) (e.g., task prepare-distribution INPUT_DIR=./brochures OUTPUT_DIR=./ready)" cmds: - - python prepare_pdf_for_distribution.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" + - uv run python prepare_pdf_for_distribution.py "{{.INPUT_DIR}}" "{{.OUTPUT_DIR}}" requires: vars: [INPUT_DIR, OUTPUT_DIR] @@ -66,6 +66,6 @@ tasks: onboard-employees: desc: "Send company policies to new employees for signature (e.g., task onboard-employees POLICIES_DIR=./policies CSV=new_hires.csv)" cmds: - - python employee_policy_onboarding.py "{{.POLICIES_DIR}}" "{{.CSV}}" + - uv run python employee_policy_onboarding.py "{{.POLICIES_DIR}}" "{{.CSV}}" requires: vars: [POLICIES_DIR, CSV] From 7a604e227506ef83c45dd51a0b9c47d02c83b1a5 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Wed, 31 Dec 2025 10:23:12 +0000 Subject: [PATCH 15/19] fixes to: use pydantic-settings for environment configuration,use typer and standardize documentation commands --- samples/python/README.md | 29 +++-- samples/python/Taskfile.yml | 5 + samples/python/api/base_client.py | 59 ++++++--- samples/python/api/platform_api.py | 9 +- samples/python/api/sign_api.py | 18 ++- samples/python/batch_process.py | 78 ++++++----- samples/python/pyproject.toml | 4 + samples/python/uv.lock | 202 +++++++++++++++++++++++++++++ 8 files changed, 335 insertions(+), 69 deletions(-) diff --git a/samples/python/README.md b/samples/python/README.md index 34d163e..d37a260 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -27,7 +27,12 @@ This project uses [uv](https://docs.astral.sh/uv/) for fast, reliable Python pac 4. Run the quickstart example: ```bash - python quickstart.py + uv run python quickstart.py + ``` + + Or use the Task command: + ```bash + task quickstart ``` ### Option 2: Using pip @@ -51,7 +56,7 @@ This project uses [uv](https://docs.astral.sh/uv/) for fast, reliable Python pac 4. Run the quickstart example: ```bash - uv run python quickstart.py + python quickstart.py ``` ## Development @@ -151,7 +156,11 @@ api/ ### Authentication ```bash +# Run directly uv run python quickstart.py + +# Or use Task command +task quickstart ``` ### Convert Documents @@ -159,8 +168,8 @@ uv run python quickstart.py # Convert DOCX to PDF uv run python convert_cli.py input.docx output.pdf pdf -# Convert PDF to DOCX -uv run python convert_cli.py input.pdf output.docx docx +# Or use Task command +task convert INPUT=input.docx OUTPUT=output.pdf FORMAT=pdf ``` ### Extract Data @@ -168,8 +177,8 @@ uv run python convert_cli.py input.pdf output.docx docx # Extract tables uv run python extract_data.py tables input.pdf output.json -# Extract forms -uv run python extract_data.py forms input.pdf output.json +# Or use Task command +task extract MODE=tables INPUT=input.pdf OUTPUT=output.json ``` ### Redact Content @@ -177,8 +186,8 @@ uv run python extract_data.py forms input.pdf output.json # Auto-detect and redact PII uv run python smart_redact_pii.py input_folder output_folder -# Redact specific keywords -uv run python redact_by_keyword.py input_folder output_folder "confidential" "secret" +# Or use Task command +task smart-redact INPUT_DIR=./input OUTPUT_DIR=./output ``` ### Batch Operations @@ -186,8 +195,8 @@ uv run python redact_by_keyword.py input_folder output_folder "confidential" "se # Convert all DOCX files to PDF uv run python batch_process.py ./input ./output pdf "*.docx" -# Password protect all PDFs -uv run python bulk_password_protect.py ./input ./output "MyPassword123" +# Or use Task command +task batch INPUT_DIR=./input OUTPUT_DIR=./output FORMAT=pdf PATTERN='*.docx' ``` ## Using the API Client diff --git a/samples/python/Taskfile.yml b/samples/python/Taskfile.yml index 01f4ec0..3e34453 100644 --- a/samples/python/Taskfile.yml +++ b/samples/python/Taskfile.yml @@ -1,6 +1,11 @@ version: '3' tasks: + quickstart: + desc: "Test API authentication (first thing to run after setup)" + cmds: + - uv run python quickstart.py + smart-redact: desc: "Smart redact PII from documents in a folder (e.g., task smart-redact INPUT_DIR=./documents OUTPUT_DIR=./redacted)" cmds: diff --git a/samples/python/api/base_client.py b/samples/python/api/base_client.py index 1e18df5..44fc2c1 100644 --- a/samples/python/api/base_client.py +++ b/samples/python/api/base_client.py @@ -2,26 +2,48 @@ from __future__ import annotations -import os import time from dataclasses import dataclass, field +from typing import Protocol import httpx -from dotenv import load_dotenv +from pydantic import BaseModel, Field +from pydantic_settings import BaseSettings, SettingsConfigDict -load_dotenv() + +class TokenResponse(BaseModel): + """OAuth2 token response model.""" + + model_config = {"populate_by_name": True} + + access_token: str = Field(alias="accessToken") + expires_in: int = Field(default=3600, alias="expiresIn") + + +class _SettingsProtocol(Protocol): # pylint: disable=too-few-public-methods + """Protocol defining required settings for OAuth clients.""" + + platform_client_id: str + platform_client_secret: str + platform_base_url: str + + +class Settings(BaseSettings): + """Application settings loaded from environment variables or .env file.""" + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + + platform_client_id: str + platform_client_secret: str + platform_base_url: str = "https://api.gonitro.dev" @dataclass class BaseOAuthClient: """Base class for API clients with OAuth2 authentication.""" - base_url: str = field( - default_factory=lambda: os.getenv("PLATFORM_BASE_URL") or "https://api.gonitro.dev" - ) - client_id: str = field(default_factory=lambda: os.getenv("PLATFORM_CLIENT_ID") or "") - client_secret: str = field( - default_factory=lambda: os.getenv("PLATFORM_CLIENT_SECRET") or "" + settings: _SettingsProtocol = field( + default_factory=lambda: Settings() # type: ignore[reportCallIssue] # pylint: disable=unnecessary-lambda ) _token: str | None = field(default=None, init=False) _token_expiry: float = field(default=0, init=False) @@ -32,15 +54,20 @@ def _get_token(self) -> str: return self._token response = httpx.post( - f"{self.base_url}/oauth/token", - json={"clientID": self.client_id, "clientSecret": self.client_secret}, + f"{self.settings.platform_base_url}/oauth/token", + json={ + "clientID": self.settings.platform_client_id, + "clientSecret": self.settings.platform_client_secret, + }, ) response.raise_for_status() - data = response.json() - token: str = data["accessToken"] - self._token = token - self._token_expiry = time.time() + data.get("expiresIn", 3600) - 60 - return token + + # Use Pydantic model for type-safe response parsing + token_data = TokenResponse.model_validate(response.json()) + + self._token = token_data.access_token + self._token_expiry = time.time() + token_data.expires_in - 60 + return token_data.access_token def get_token(self) -> str: """Public method to get authentication token. diff --git a/samples/python/api/platform_api.py b/samples/python/api/platform_api.py index f2da1e1..2d5195c 100644 --- a/samples/python/api/platform_api.py +++ b/samples/python/api/platform_api.py @@ -38,7 +38,7 @@ def _request( data = {"method": method, "params": json.dumps(params or {})} response = httpx.post( - f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data + f"{self.settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data ) response.raise_for_status() @@ -62,7 +62,7 @@ def _request_bytes( data = {"method": method, "params": json.dumps(params or {})} response = httpx.post( - f"{self.base_url}/{endpoint}", headers=headers, files=files, data=data + f"{self.settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data ) response.raise_for_status() @@ -138,7 +138,10 @@ def merge(self, file_paths: list[Path]) -> bytes: data = {"method": "merge", "params": "{}"} response = httpx.post( - f"{self.base_url}/transformations", headers=headers, files=files, data=data + f"{self.settings.platform_base_url}/transformations", + headers=headers, + files=files, + data=data, ) response.raise_for_status() return response.content diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py index bd0585d..730a73e 100644 --- a/samples/python/api/sign_api.py +++ b/samples/python/api/sign_api.py @@ -31,7 +31,7 @@ def _request( response = httpx.request( method=method, - url=f"{self.base_url}{endpoint}", + url=f"{self.settings.platform_base_url}{endpoint}", headers=headers, json=json_data, params=params, @@ -44,7 +44,7 @@ def _request( try: error_detail = response.json() print(f" ❌ API Error Response: {error_detail}") - except Exception: # noqa: BLE001 + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught print(f" ❌ API Error (no JSON): {response.text}") raise @@ -57,7 +57,10 @@ def _request_bytes( headers = {"Authorization": f"Bearer {self._get_token()}"} response = httpx.request( - method=method, url=f"{self.base_url}{endpoint}", headers=headers, params=params + method=method, + url=f"{self.settings.platform_base_url}{endpoint}", + headers=headers, + params=params, ) response.raise_for_status() @@ -126,7 +129,10 @@ def delete_envelope(self, envelope_id: str) -> None: envelope_id: UUID of the envelope """ headers = {"Authorization": f"Bearer {self._get_token()}"} - response = httpx.delete(f"{self.base_url}/sign/envelopes/{envelope_id}", headers=headers) + response = httpx.delete( + f"{self.settings.platform_base_url}/sign/envelopes/{envelope_id}", + headers=headers, + ) response.raise_for_status() # ========== Document Management ========== @@ -163,7 +169,9 @@ def create_document( headers = {"Authorization": f"Bearer {self._get_token()}"} response = httpx.post( - f"{self.base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files + f"{self.settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", + headers=headers, + files=files, ) response.raise_for_status() diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index 37d8fd5..9dd5bdb 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -29,8 +29,10 @@ python batch_process.py ./documents ./converted png "*" """ -import sys -from pathlib import Path +from pathlib import Path # noqa: TC003 +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup @@ -38,31 +40,37 @@ # Supported output formats SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx"] - -def main() -> None: +app = typer.Typer() + + +@app.command() +def main( + input_folder: Annotated[ + Path, typer.Argument(help="Input folder containing documents to convert") + ], + output_folder: Annotated[Path, typer.Argument(help="Output folder for converted documents")], + to_format: Annotated[ + str, + typer.Argument( + help=f"Target format for conversion. Supported: {', '.join(SUPPORTED_FORMATS)}" + ), + ], + pattern: Annotated[ + str, + typer.Argument(help="File pattern to match (e.g., '*.docx', '*.pdf', '*')"), + ] = "*", +) -> None: """Process multiple documents in batch, converting them to a specified format.""" - # Check command-line arguments - if len(sys.argv) < 4: - print("Usage: python batch_process.py [pattern]") - print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - print("Example: python batch_process.py ./docs ./output pdf '*.docx'") - sys.exit(1) - - # Get folder paths and format from arguments - input_folder = Path(sys.argv[1]) - output_folder = Path(sys.argv[2]) - to_format = sys.argv[3].lower() - pattern = sys.argv[4] if len(sys.argv) > 4 else "*" - # Validate output format - if to_format not in SUPPORTED_FORMATS: - print(f"❌ Error: Unsupported format '{to_format}'") - print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - sys.exit(1) + to_format_lower = to_format.lower() + if to_format_lower not in SUPPORTED_FORMATS: + typer.echo(f"❌ Error: Unsupported format '{to_format}'") + typer.echo(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") + raise typer.Exit(code=1) # Validate and setup with custom pattern files = validate_and_setup(input_folder, output_folder, file_patterns=[pattern]) - print(f"📋 Found {len(files)} file(s) matching '{pattern}'\n") + typer.echo(f"📋 Found {len(files)} file(s) matching '{pattern}'\n") # Initialize API client (loads credentials from .env) client = PlatformAPIClient() @@ -72,32 +80,32 @@ def main() -> None: failed_count = 0 for i, file_path in enumerate(files, 1): - print(f"[{i}/{len(files)}] Processing: {file_path.name}") + typer.echo(f"[{i}/{len(files)}] Processing: {file_path.name}") try: # Convert to target format - print(f" 🔄 Converting to {to_format.upper()}...") - converted = client.convert(file_path, to_format) + typer.echo(f" 🔄 Converting to {to_format_lower.upper()}...") + converted = client.convert(file_path, to_format_lower) # Save converted file - output_file = output_folder / f"{file_path.stem}.{to_format}" + output_file = output_folder / f"{file_path.stem}.{to_format_lower}" output_file.write_bytes(converted) - print(f" ✅ Converted: {output_file.name}\n") + typer.echo(f" ✅ Converted: {output_file.name}\n") success_count += 1 - except Exception as e: # noqa: BLE001 - print(f" ❌ FAILED: {e}\n") + except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + typer.echo(f" ❌ FAILED: {e}\n") failed_count += 1 # Display summary - print("=" * 60) - print(f"✅ {success_count} file(s) converted to {to_format.upper()}") + typer.echo("=" * 60) + typer.echo(f"✅ {success_count} file(s) converted to {to_format_lower.upper()}") if failed_count > 0: - print(f"⚠️ {failed_count} file(s) FAILED to convert!") - print(f"📂 Output: {output_folder.absolute()}") - print("=" * 60) + typer.echo(f"⚠️ {failed_count} file(s) FAILED to convert!") + typer.echo(f"📂 Output: {output_folder.absolute()}") + typer.echo("=" * 60) if __name__ == "__main__": - main() + app() diff --git a/samples/python/pyproject.toml b/samples/python/pyproject.toml index b424963..6a513a6 100644 --- a/samples/python/pyproject.toml +++ b/samples/python/pyproject.toml @@ -13,8 +13,11 @@ readme = "README.md" requires-python = ">=3.14" dependencies = [ "httpx>=0.27.0", + "pydantic>=2.0.0", + "pydantic-settings>=2.0.0", "python-dotenv>=1.0.0", "reportlab>=4.0.0", + "typer>=0.9.0", ] [tool.ruff] format.preview = true @@ -108,4 +111,5 @@ dev = [ "pyenchant>=3.3.0", "pylint>=4.0.4", "pyright>=1.1.407", + "ruff>=0.8.0", ] \ No newline at end of file diff --git a/samples/python/uv.lock b/samples/python/uv.lock index 84878b1..b94dd7b 100644 --- a/samples/python/uv.lock +++ b/samples/python/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 2 requires-python = ">=3.14" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "anyio" version = "4.12.0" @@ -57,6 +66,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -130,6 +151,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -139,14 +172,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "nitro-platform-samples" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "python-dotenv" }, { name = "reportlab" }, + { name = "typer" }, ] [package.optional-dependencies] @@ -154,16 +199,21 @@ dev = [ { name = "pyenchant" }, { name = "pylint" }, { name = "pyright" }, + { name = "ruff" }, ] [package.metadata] requires-dist = [ { name = "httpx", specifier = ">=0.27.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pydantic-settings", specifier = ">=2.0.0" }, { name = "pyenchant", marker = "extra == 'dev'", specifier = ">=3.3.0" }, { name = "pylint", marker = "extra == 'dev'", specifier = ">=4.0.4" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.407" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "reportlab", specifier = ">=4.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, + { name = "typer", specifier = ">=0.9.0" }, ] provides-extras = ["dev"] @@ -218,6 +268,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + [[package]] name = "pyenchant" version = "3.3.0" @@ -229,6 +347,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/ae/5624803b62ecb0a20248f0d28ed3f78c78746a032582a016d4b2890c7899/pyenchant-3.3.0-py3-none-win_amd64.whl", hash = "sha256:04a5bd0e022ebe2e8c6d9e498ec3d650602e264ec5486e9c6a1b7f99c9507c49", size = 37427576, upload-time = "2025-09-14T16:23:09.574Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pylint" version = "4.0.4" @@ -282,6 +409,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263, upload-time = "2025-12-21T11:50:08.93Z" }, ] +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "tomlkit" version = "0.13.3" @@ -291,6 +466,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] +[[package]] +name = "typer" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/30/ff9ede605e3bd086b4dd842499814e128500621f7951ca1e5ce84bbf61b1/typer-0.21.0.tar.gz", hash = "sha256:c87c0d2b6eee3b49c5c64649ec92425492c14488096dfbc8a0c2799b2f6f9c53", size = 106781, upload-time = "2025-12-25T09:54:53.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e4/5ebc1899d31d2b1601b32d21cfb4bba022ae6fce323d365f0448031b1660/typer-0.21.0-py3-none-any.whl", hash = "sha256:c79c01ca6b30af9fd48284058a7056ba0d3bf5cf10d0ff3d0c5b11b68c258ac6", size = 47109, upload-time = "2025-12-25T09:54:51.918Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -299,3 +489,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] From 89b1b17aed51538bf737a8cb573d205d49b5b824 Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Sun, 4 Jan 2026 21:58:43 +0000 Subject: [PATCH 16/19] updated scripts to use typer --- samples/python/batch_process.py | 2 +- samples/python/bulk_password_protect.py | 31 +++---- samples/python/convert_cli.py | 62 +++++++------ samples/python/employee_policy_onboarding.py | 93 ++++++++++--------- samples/python/extract_data.py | 88 +++++++++--------- .../helper_functions/document_helpers.py | 2 +- .../python/helper_functions/sign_helpers.py | 4 +- .../python/prepare_pdf_for_distribution.py | 25 ++--- samples/python/pyproject.toml | 2 +- samples/python/redact_by_keyword.py | 80 ++++++++-------- samples/python/smart_redact_pii.py | 24 ++--- 11 files changed, 207 insertions(+), 206 deletions(-) diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index 9dd5bdb..e2615cf 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -29,7 +29,7 @@ python batch_process.py ./documents ./converted png "*" """ -from pathlib import Path # noqa: TC003 +from pathlib import Path from typing import Annotated import typer diff --git a/samples/python/bulk_password_protect.py b/samples/python/bulk_password_protect.py index b3d7429..7ad3254 100644 --- a/samples/python/bulk_password_protect.py +++ b/samples/python/bulk_password_protect.py @@ -29,29 +29,28 @@ python bulk_password_protect.py ../../test_files/test-pdfs ./output MySecureP@ss123 """ -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup +app = typer.Typer() -def main() -> None: - """Apply password protection to all PDF files in a directory.""" - # Check command-line arguments - if len(sys.argv) != 4: - print("Usage: python bulk_password_protect.py ") - sys.exit(1) - - # Get folder paths and password from arguments - input_folder = Path(sys.argv[1]) - output_folder = Path(sys.argv[2]) - password = sys.argv[3] +@app.command() +def main( + input_folder: Annotated[Path, typer.Argument(help='Input folder containing PDF documents')], + output_folder: Annotated[Path, typer.Argument(help='Output folder for protected PDFs')], + password: Annotated[str, typer.Argument(help='Password for protection (min 6 characters)')], +) -> None: + """Apply password protection to all PDF files in a directory.""" # Validate password strength if len(password) < 6: - print("❌ Error: Password must be at least 6 characters long") - sys.exit(1) + print('❌ Error: Password must be at least 6 characters long') + raise typer.Exit(code=1) # Validate and setup (only process PDF files) files = validate_and_setup(input_folder, output_folder, file_patterns=["*.pdf"]) @@ -93,5 +92,5 @@ def main() -> None: print("=" * 60) -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/convert_cli.py b/samples/python/convert_cli.py index 76c50f6..05cc5b0 100644 --- a/samples/python/convert_cli.py +++ b/samples/python/convert_cli.py @@ -28,8 +28,10 @@ python convert_cli.py spreadsheet.xlsx data.pdf pdf """ -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient @@ -37,55 +39,57 @@ SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx", "png"] -def main() -> None: - """Convert a document from one format to another using the Platform API.""" - # Check command-line arguments - if len(sys.argv) != 4: - print("Usage: python convert_cli.py ") - print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - print("Example: python convert_cli.py document.docx document.pdf pdf") - sys.exit(1) +app = typer.Typer() - # Get file paths and format from arguments - input_path = Path(sys.argv[1]) - output_path = Path(sys.argv[2]) - to_format = sys.argv[3].lower() + +@app.command() +def main( + input_file: Annotated[Path, typer.Argument(help='Input file to convert')], + output_file: Annotated[Path, typer.Argument(help='Output file path')], + to_format: Annotated[ + str, + typer.Argument(help=f"Target format. Supported: {', '.join(SUPPORTED_FORMATS)}"), + ], +) -> None: + """Convert a document from one format to another using the Platform API.""" + # Normalize format to lowercase + to_format = to_format.lower() # Validate input file exists - if not input_path.exists(): - print(f"❌ Error: Input file not found: {input_path}") - sys.exit(1) + if not input_file.exists(): + print(f'❌ Error: Input file not found: {input_file}') + raise typer.Exit(code=1) # Validate output format if to_format not in SUPPORTED_FORMATS: print(f"❌ Error: Unsupported format '{to_format}'") print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - sys.exit(1) + raise typer.Exit(code=1) # Create output directory if needed - output_path.parent.mkdir(parents=True, exist_ok=True) + output_file.parent.mkdir(parents=True, exist_ok=True) # Initialize API client (loads credentials from .env) client = PlatformAPIClient() try: # Convert document - print(f"🔄 Converting {input_path.name} to {to_format.upper()}...") - converted = client.convert(input_path, to_format) + print(f'🔄 Converting {input_file.name} to {to_format.upper()}...') + converted = client.convert(input_file, to_format) # Save converted file - output_path.write_bytes(converted) + output_file.write_bytes(converted) # Display success message - print("✅ Conversion successful!") - print(f"📄 Input: {input_path.name} ({input_path.stat().st_size:,} bytes)") - print(f"📄 Output: {output_path.name} ({len(converted):,} bytes)") - print(f"📂 Saved to: {output_path.absolute()}") + print('✅ Conversion successful!') + print(f'📄 Input: {input_file.name} ({input_file.stat().st_size:,} bytes)') + print(f'📄 Output: {output_file.name} ({len(converted):,} bytes)') + print(f'📂 Saved to: {output_file.absolute()}') except Exception as e: # noqa: BLE001 - print(f"❌ Conversion FAILED: {e}") - sys.exit(1) + print(f'❌ Conversion FAILED: {e}') + raise typer.Exit(code=1) from None -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/employee_policy_onboarding.py b/samples/python/employee_policy_onboarding.py index e18fccf..b28d52c 100644 --- a/samples/python/employee_policy_onboarding.py +++ b/samples/python/employee_policy_onboarding.py @@ -45,9 +45,10 @@ ├── signed-documents/ """ -import sys from pathlib import Path -from typing import Any +from typing import Annotated, Any + +import typer from api.sign_api import SignAPIClient from helper_functions.sign_helpers import ( @@ -66,6 +67,9 @@ class EnvelopeNotSignedError(Exception): """Raised when an envelope was not signed in time.""" +app = typer.Typer() + + def _validate_and_setup_inputs( policies_folder: Path, employees_csv: Path ) -> tuple[Path, list[dict[str, str]], list[dict[str, Any]]]: @@ -80,27 +84,27 @@ def _validate_and_setup_inputs( """ # Validate inputs if not policies_folder.exists() or not policies_folder.is_dir(): - print(f"❌ Policies folder not found: {policies_folder}") - sys.exit(1) + print(f'❌ Policies folder not found: {policies_folder}') + raise typer.Exit(code=1) if not employees_csv.exists(): - print(f"❌ Employees CSV not found: {employees_csv}") - sys.exit(1) + print(f'❌ Employees CSV not found: {employees_csv}') + raise typer.Exit(code=1) # Display header - output_folder = Path("output") - print("=" * 60) - print("📝 SEND POLICIES TO EMPLOYEES") - print("=" * 60) - print(f"Policies: {policies_folder}") - print(f"Employees: {employees_csv}") - print(f"Output: {output_folder}") - print("=" * 60) + output_folder = Path('output') + print('=' * 60) + print('📝 SEND POLICIES TO EMPLOYEES') + print('=' * 60) + print(f'Policies: {policies_folder}') + print(f'Employees: {employees_csv}') + print(f'Output: {output_folder}') + print('=' * 60) print() # Load employees and documents employees = load_employees_from_csv(employees_csv) - print(f"👥 Found {len(employees)} employee(s)\n") + print(f'👥 Found {len(employees)} employee(s)\n') documents = load_policy_documents_from_folder(policies_folder) @@ -133,54 +137,53 @@ def _process_employee_onboarding( EnvelopeNotSignedError: If envelope is not signed within timeout Exception: For other errors during processing """ - name = employee["name"] - email = employee["email"] + name = employee['name'] + email = employee['email'] - print(f"\n[{employee_num}/{total_employees}] {name}") + print(f'\n[{employee_num}/{total_employees}] {name}') # Create employee-specific output folder employee_folder = output_folder / create_employee_folder_name(name) employee_folder.mkdir(parents=True, exist_ok=True) # Create envelope and upload documents - log_step("📝 Creating envelope...") + log_step('📝 Creating envelope...') envelope_id, document_ids = create_signature_envelope(sign_client, documents, name, email) # Add participant (signer) - log_step("👤 Adding signer...") + log_step('👤 Adding signer...') participant_id = sign_client.create_participant( - envelope_id, {"email": email, "role": "signer", "name": name} - )["ID"] + envelope_id, {'email': email, 'role': 'signer', 'name': name} + )['ID'] # Add signature fields to all documents - log_step("✍️ Adding fields...") + log_step('✍️ Adding fields...') add_signature_fields_to_documents(sign_client, envelope_id, document_ids, participant_id) # Send and monitor envelope - log_step("📤 Sending...") + log_step('📤 Sending...') status = send_and_monitor_envelope(sign_client, envelope_id, email, timeout_minutes=60) - if status != "sealed": - raise EnvelopeNotSignedError(f"Envelope not signed: {status}") + if status != 'sealed': + raise EnvelopeNotSignedError(f'Envelope not signed: {status}') # Download signed documents - log_step("📥 Downloading...") - download_signed_document(sign_client, envelope_id, employee_folder, "signed-policies.zip") + log_step('📥 Downloading...') + download_signed_document(sign_client, envelope_id, employee_folder, 'signed-policies.zip') - print(" ✅ Completed\n") + print(' ✅ Completed\n') -def main() -> None: +@app.command() +def main( + policies_folder: Annotated[ + Path, typer.Argument(help='Folder containing policy PDF documents') + ], + employees_csv: Annotated[ + Path, typer.Argument(help='CSV file with employee data (name,email columns)') + ], +) -> None: """Send company policy documents to employees for electronic signature via Sign API.""" - # Check command-line arguments - if len(sys.argv) != 3: - print("Usage: python employee_policy_onboarding.py ") - sys.exit(1) - - # Parse arguments - policies_folder = Path(sys.argv[1]) - employees_csv = Path(sys.argv[2]) - try: # Validate inputs and load data output_folder, employees, documents = _validate_and_setup_inputs( @@ -223,12 +226,12 @@ def main() -> None: print("=" * 60) except KeyboardInterrupt: - print("\n\n⚠️ Interrupted by user") - sys.exit(1) + print('\n\n⚠️ Interrupted by user') + raise typer.Exit(code=1) from None except Exception as e: # noqa: BLE001 - print(f"\n❌ Error: {e}") - sys.exit(1) + print(f'\n❌ Error: {e}') + raise typer.Exit(code=1) from None -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/extract_data.py b/samples/python/extract_data.py index 8238966..3e5e5b1 100644 --- a/samples/python/extract_data.py +++ b/samples/python/extract_data.py @@ -35,80 +35,80 @@ """ import json -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient +app = typer.Typer() + -def main() -> None: +@app.command() +def main( + mode: Annotated[str, typer.Argument(help="Extraction mode: 'forms' or 'tables'")], + input_pdf: Annotated[Path, typer.Argument(help='Input PDF file')], + output_json: Annotated[Path, typer.Argument(help='Output JSON file')], +) -> None: """Extract structured data (forms or tables) from PDF documents.""" - # Check command-line arguments - if len(sys.argv) != 4: - print("Usage: python extract_data.py ") - print("Modes: forms, tables") - print("Example: python extract_data.py forms application.pdf data.json") - sys.exit(1) - - # Get mode and file paths from arguments - mode = sys.argv[1].lower() - input_path = Path(sys.argv[2]) - output_path = Path(sys.argv[3]) + # Normalize mode to lowercase + mode = mode.lower() # Validate mode - if mode not in ["forms", "tables"]: + if mode not in ['forms', 'tables']: print("❌ Error: Mode must be 'forms' or 'tables'") - sys.exit(1) + raise typer.Exit(code=1) # Validate input file exists - if not input_path.exists(): - print(f"❌ Error: Input file not found: {input_path}") - sys.exit(1) + if not input_pdf.exists(): + print(f'❌ Error: Input file not found: {input_pdf}') + raise typer.Exit(code=1) # Validate input is a PDF - if input_path.suffix.lower() != ".pdf": - print("❌ Error: Input must be a PDF file") - sys.exit(1) + if input_pdf.suffix.lower() != '.pdf': + print('❌ Error: Input must be a PDF file') + raise typer.Exit(code=1) # Create output directory if needed - output_path.parent.mkdir(parents=True, exist_ok=True) + output_json.parent.mkdir(parents=True, exist_ok=True) # Initialize API client (loads credentials from .env) client = PlatformAPIClient() try: # Extract data based on mode - if mode == "forms": - print(f"📋 Extracting form fields from {input_path.name}...") - data = client.extract_forms(input_path) - data_type = "form fields" + if mode == 'forms': + print(f'📋 Extracting form fields from {input_pdf.name}...') + data = client.extract_forms(input_pdf) + data_type = 'form fields' - else: # mode == "tables" - print(f"📊 Extracting table data from {input_path.name}...") - data = client.extract_tables(input_path) - data_type = "tables" + else: # mode == 'tables' + print(f'📊 Extracting table data from {input_pdf.name}...') + data = client.extract_tables(input_pdf) + data_type = 'tables' # Count extracted items - result = data.get("result", {}) - if mode == "forms": - item_count = len(result.get("fields", [])) + result = data.get('result', {}) + if mode == 'forms': + item_count = len(result.get('fields', [])) else: - item_count = len(result.get("tables", [])) + item_count = len(result.get('tables', [])) # Save extracted data as JSON - output_path.write_text(json.dumps(data, indent=2), encoding="utf-8") + output_json.write_text(json.dumps(data, indent=2), encoding='utf-8') # Display success message - print("✅ Extraction successful!") - print(f"📊 Extracted: {item_count} {data_type}") - print(f"📄 Input: {input_path.name}") - print(f"📄 Output: {output_path.name}") - print(f"📂 Saved to: {output_path.absolute()}") + print('✅ Extraction successful!') + print(f'📊 Extracted: {item_count} {data_type}') + print(f'📄 Input: {input_pdf.name}') + print(f'📄 Output: {output_json.name}') + print(f'📂 Saved to: {output_json.absolute()}') except Exception as e: # noqa: BLE001 - print(f"❌ Extraction FAILED: {e}") - sys.exit(1) + print(f'❌ Extraction FAILED: {e}') + raise typer.Exit(code=1) from None -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/helper_functions/document_helpers.py b/samples/python/helper_functions/document_helpers.py index 952bf42..3be16a4 100644 --- a/samples/python/helper_functions/document_helpers.py +++ b/samples/python/helper_functions/document_helpers.py @@ -5,7 +5,7 @@ from __future__ import annotations import sys -from pathlib import Path # noqa: TC003 +from pathlib import Path def validate_and_setup( diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py index ece34d2..0a31b84 100644 --- a/samples/python/helper_functions/sign_helpers.py +++ b/samples/python/helper_functions/sign_helpers.py @@ -9,7 +9,7 @@ import time import zipfile from datetime import UTC, datetime -from pathlib import Path # noqa: TC003 +from pathlib import Path from typing import TYPE_CHECKING, Any import httpx @@ -126,7 +126,7 @@ def _upload_documents_to_envelope( headers = {"Authorization": f"Bearer {token}"} response = httpx.post( - f"{sign_client.base_url}/sign/envelopes/{envelope_id}/documents", + f"{sign_client.settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files, ) diff --git a/samples/python/prepare_pdf_for_distribution.py b/samples/python/prepare_pdf_for_distribution.py index b44e894..a44a15e 100755 --- a/samples/python/prepare_pdf_for_distribution.py +++ b/samples/python/prepare_pdf_for_distribution.py @@ -30,8 +30,10 @@ python prepare_pdf_for_distribution.py ../../test_files/test-batch ./output """ -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup @@ -40,16 +42,15 @@ PROPERTIES_TO_REMOVE = ["title", "author", "subject", "keywords", "creator", "producer"] -def main() -> None: - """Prepare documents for distribution by converting to PDF and removing metadata.""" - # Check command-line arguments - if len(sys.argv) != 3: - print("Usage: python prepare_pdf_for_distribution.py ") - sys.exit(1) +app = typer.Typer() - # Get folder paths from arguments - input_folder = Path(sys.argv[1]) - output_folder = Path(sys.argv[2]) + +@app.command() +def main( + input_folder: Annotated[Path, typer.Argument(help='Input folder containing documents')], + output_folder: Annotated[Path, typer.Argument(help='Output folder for prepared PDFs')], +) -> None: + """Prepare documents for distribution by converting to PDF and removing metadata.""" # Validate and setup files = validate_and_setup(input_folder, output_folder) @@ -108,5 +109,5 @@ def main() -> None: print("=" * 60) -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/pyproject.toml b/samples/python/pyproject.toml index 6a513a6..2769f9f 100644 --- a/samples/python/pyproject.toml +++ b/samples/python/pyproject.toml @@ -59,7 +59,7 @@ lint.select = [ "YTT", # flake8-2020 "T10", # debugger statements ] -lint.ignore = ["S101", "S311", "TRY003", "TC006"] +lint.ignore = ["S101", "S311", "TRY003", "TC003", "TC006"] lint.flake8-type-checking.strict = true [tool.pyright] diff --git a/samples/python/redact_by_keyword.py b/samples/python/redact_by_keyword.py index c2deacd..06c21b9 100644 --- a/samples/python/redact_by_keyword.py +++ b/samples/python/redact_by_keyword.py @@ -33,64 +33,58 @@ python redact_by_keyword.py report.pdf clean.pdf "Project Zeus" "Client ABC" """ -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient +app = typer.Typer() -def main() -> None: - """Redact specific keywords from PDF documents using text search.""" - # Check command-line arguments - if len(sys.argv) < 4: - print( - "Usage: python redact_by_keyword.py [keyword2 ...]" - ) - print( - "Example: python redact_by_keyword.py document.pdf redacted.pdf 'confidential' 'secret'" - ) - sys.exit(1) - - # Get file paths and keywords from arguments - input_path = Path(sys.argv[1]) - output_path = Path(sys.argv[2]) - keywords = sys.argv[3:] +@app.command() +def main( + input_pdf: Annotated[Path, typer.Argument(help='Input PDF file to redact')], + output_pdf: Annotated[Path, typer.Argument(help='Output PDF file with redactions')], + keywords: Annotated[list[str], typer.Argument(help='Keywords to search for and redact')], +) -> None: + """Redact specific keywords from PDF documents using text search.""" # Validate input file exists - if not input_path.exists(): - print(f"❌ Error: Input file not found: {input_path}") - sys.exit(1) + if not input_pdf.exists(): + print(f'❌ Error: Input file not found: {input_pdf}') + raise typer.Exit(code=1) # Validate input is a PDF - if input_path.suffix.lower() != ".pdf": - print("❌ Error: Input must be a PDF file") - sys.exit(1) + if input_pdf.suffix.lower() != '.pdf': + print('❌ Error: Input must be a PDF file') + raise typer.Exit(code=1) # Create output directory if needed - output_path.parent.mkdir(parents=True, exist_ok=True) + output_pdf.parent.mkdir(parents=True, exist_ok=True) # Initialize API client (loads credentials from .env) client = PlatformAPIClient() try: # Step 1: Search for keywords in document - print(f"🔍 Searching for {len(keywords)} keyword(s) in {input_path.name}...") + print(f'🔍 Searching for {len(keywords)} keyword(s) in {input_pdf.name}...') print(f" Keywords: {', '.join(repr(k) for k in keywords)}") - bbox_data = client.find_text_boxes(input_path, keywords) + bbox_data = client.find_text_boxes(input_pdf, keywords) # Extract text box locations from response - text_boxes = bbox_data.get("result", {}).get("textBoxes", []) + text_boxes = bbox_data.get('result', {}).get('textBoxes', []) if not text_boxes: - print("ℹ️ No keyword matches found - copying original file") # noqa: RUF001 + print('ℹ️ No keyword matches found - copying original file') # noqa: RUF001 # Copy original file to output if no keywords found - output_path.write_bytes(input_path.read_bytes()) - print(f"✅ Saved: {output_path.name}") - print(f"📂 Output: {output_path.absolute()}") + output_pdf.write_bytes(input_pdf.read_bytes()) + print(f'✅ Saved: {output_pdf.name}') + print(f'📂 Output: {output_pdf.absolute()}') return - print(f"🎯 Found {len(text_boxes)} keyword instance(s) to redact") + print(f'🎯 Found {len(text_boxes)} keyword instance(s) to redact') # Step 2: Prepare redaction coordinates print("🔒 Applying redactions...") @@ -99,22 +93,22 @@ def main() -> None: ] # Step 3: Apply redactions to document - redacted_pdf = client.redact(input_path, redactions) + redacted_pdf = client.redact(input_pdf, redactions) # Save redacted PDF - output_path.write_bytes(redacted_pdf) + output_pdf.write_bytes(redacted_pdf) # Display success message - print("✅ Redaction successful!") - print(f"🔒 Redacted: {len(text_boxes)} instance(s)") - print(f"📄 Input: {input_path.name}") - print(f"📄 Output: {output_path.name}") - print(f"📂 Saved to: {output_path.absolute()}") + print('✅ Redaction successful!') + print(f'🔒 Redacted: {len(text_boxes)} instance(s)') + print(f'📄 Input: {input_pdf.name}') + print(f'📄 Output: {output_pdf.name}') + print(f'📂 Saved to: {output_pdf.absolute()}') except Exception as e: # noqa: BLE001 - print(f"❌ Redaction FAILED: {e}") - sys.exit(1) + print(f'❌ Redaction FAILED: {e}') + raise typer.Exit(code=1) from None -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() diff --git a/samples/python/smart_redact_pii.py b/samples/python/smart_redact_pii.py index 43679b4..c3c0718 100644 --- a/samples/python/smart_redact_pii.py +++ b/samples/python/smart_redact_pii.py @@ -29,23 +29,23 @@ python smart_redact_pii.py ../../test_files/test-pdfs ./output """ -import sys from pathlib import Path +from typing import Annotated + +import typer from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup +app = typer.Typer() -def main() -> None: - """Automatically detect and redact PII (personally identifiable information) from PDFs.""" - # Check command-line arguments - if len(sys.argv) != 3: - print("Usage: python smart_redact_pii.py ") - sys.exit(1) - # Get folder paths from arguments - input_folder = Path(sys.argv[1]) - output_folder = Path(sys.argv[2]) +@app.command() +def main( + input_folder: Annotated[Path, typer.Argument(help='Input folder containing PDF documents')], + output_folder: Annotated[Path, typer.Argument(help='Output folder for redacted PDFs')], +) -> None: + """Automatically detect and redact PII (personally identifiable information) from PDFs.""" # Validate and setup (only process PDF files) files = validate_and_setup(input_folder, output_folder, file_patterns=["*.pdf"]) @@ -116,5 +116,5 @@ def main() -> None: print("=" * 60) -if __name__ == "__main__": - main() +if __name__ == '__main__': + app() From bd69f4ee7c3c5b16bfb2037f5f34e38e3dc6942d Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Sun, 4 Jan 2026 23:38:40 +0000 Subject: [PATCH 17/19] updated settings to be private, added reusable httpx.client, created enum fot format validation --- samples/python/api/base_client.py | 18 ++++++----- samples/python/api/platform_api.py | 14 ++++----- samples/python/api/sign_api.py | 19 +++++------- samples/python/batch_process.py | 31 +++++++++---------- samples/python/convert_cli.py | 28 ++++++++--------- .../python/helper_functions/sign_helpers.py | 4 +-- 6 files changed, 55 insertions(+), 59 deletions(-) diff --git a/samples/python/api/base_client.py b/samples/python/api/base_client.py index 44fc2c1..193159a 100644 --- a/samples/python/api/base_client.py +++ b/samples/python/api/base_client.py @@ -11,6 +11,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict +TOKEN_EXPIRY_BUFFER_SECONDS = 60 + + class TokenResponse(BaseModel): """OAuth2 token response model.""" @@ -42,31 +45,32 @@ class Settings(BaseSettings): class BaseOAuthClient: """Base class for API clients with OAuth2 authentication.""" - settings: _SettingsProtocol = field( + _settings: _SettingsProtocol = field( default_factory=lambda: Settings() # type: ignore[reportCallIssue] # pylint: disable=unnecessary-lambda ) _token: str | None = field(default=None, init=False) _token_expiry: float = field(default=0, init=False) + _client: httpx.Client = field(default_factory=httpx.Client, init=False) def _get_token(self) -> str: """Get or refresh OAuth2 access token.""" if self._token and time.time() < self._token_expiry: return self._token - response = httpx.post( - f"{self.settings.platform_base_url}/oauth/token", + response = self._client.post( + f"{self._settings.platform_base_url}/oauth/token", json={ - "clientID": self.settings.platform_client_id, - "clientSecret": self.settings.platform_client_secret, + "clientID": self._settings.platform_client_id, + "clientSecret": self._settings.platform_client_secret, }, ) response.raise_for_status() # Use Pydantic model for type-safe response parsing - token_data = TokenResponse.model_validate(response.json()) + token_data = TokenResponse.model_validate_json(response.content) self._token = token_data.access_token - self._token_expiry = time.time() + token_data.expires_in - 60 + self._token_expiry = time.time() + token_data.expires_in - TOKEN_EXPIRY_BUFFER_SECONDS return token_data.access_token def get_token(self) -> str: diff --git a/samples/python/api/platform_api.py b/samples/python/api/platform_api.py index 2d5195c..312dd3e 100644 --- a/samples/python/api/platform_api.py +++ b/samples/python/api/platform_api.py @@ -37,8 +37,8 @@ def _request( files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} data = {"method": method, "params": json.dumps(params or {})} - response = httpx.post( - f"{self.settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data + response = self._client.post( + f"{self._settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data ) response.raise_for_status() @@ -61,8 +61,8 @@ def _request_bytes( files = {"file": (file_path.name, file_path.read_bytes(), mime_type)} data = {"method": method, "params": json.dumps(params or {})} - response = httpx.post( - f"{self.settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data + response = self._client.post( + f"{self._settings.platform_base_url}/{endpoint}", headers=headers, files=files, data=data ) response.raise_for_status() @@ -134,11 +134,11 @@ def merge(self, file_paths: list[Path]) -> bytes: # Open files with context manager opened_files = [fp.open("rb") for fp in file_paths] try: - files = [("file", f) for f in opened_files] + files = [("file", (f.name, f)) for f in opened_files] data = {"method": "merge", "params": "{}"} - response = httpx.post( - f"{self.settings.platform_base_url}/transformations", + response = self._client.post( + f"{self._settings.platform_base_url}/transformations", headers=headers, files=files, data=data, diff --git a/samples/python/api/sign_api.py b/samples/python/api/sign_api.py index 730a73e..f596222 100644 --- a/samples/python/api/sign_api.py +++ b/samples/python/api/sign_api.py @@ -29,9 +29,9 @@ def _request( """Make authenticated API request returning JSON.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - response = httpx.request( + response = self._client.request( method=method, - url=f"{self.settings.platform_base_url}{endpoint}", + url=f"{self._settings.platform_base_url}{endpoint}", headers=headers, json=json_data, params=params, @@ -56,9 +56,9 @@ def _request_bytes( """Make authenticated API request returning binary data.""" headers = {"Authorization": f"Bearer {self._get_token()}"} - response = httpx.request( + response = self._client.request( method=method, - url=f"{self.settings.platform_base_url}{endpoint}", + url=f"{self._settings.platform_base_url}{endpoint}", headers=headers, params=params, ) @@ -129,8 +129,8 @@ def delete_envelope(self, envelope_id: str) -> None: envelope_id: UUID of the envelope """ headers = {"Authorization": f"Bearer {self._get_token()}"} - response = httpx.delete( - f"{self.settings.platform_base_url}/sign/envelopes/{envelope_id}", + response = self._client.delete( + f"{self._settings.platform_base_url}/sign/envelopes/{envelope_id}", headers=headers, ) response.raise_for_status() @@ -168,8 +168,8 @@ def create_document( headers = {"Authorization": f"Bearer {self._get_token()}"} - response = httpx.post( - f"{self.settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", + response = self._client.post( + f"{self._settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files, ) @@ -229,7 +229,6 @@ def send_for_signing(self, envelope_id: str) -> dict[str, Any]: Returns: Updated envelope with 'sent' status """ - # The correct endpoint uses a colon before 'send-for-signing' return self._request("POST", f"/sign/envelopes/{envelope_id}:send-for-signing") def cancel_envelope(self, envelope_id: str) -> dict[str, Any]: @@ -265,7 +264,6 @@ def download_sealed_envelope(self, envelope_id: str) -> bytes: Returns: PDF bytes of the sealed document """ - # The correct endpoint uses a colon before 'download-sealed' return self._request_bytes("GET", f"/sign/envelopes/{envelope_id}:download-sealed") def download_original_envelope(self, envelope_id: str) -> bytes: @@ -277,5 +275,4 @@ def download_original_envelope(self, envelope_id: str) -> bytes: Returns: Original document bytes """ - # The correct endpoint uses a colon before 'download-original' return self._request_bytes("GET", f"/sign/envelopes/{envelope_id}:download-original") diff --git a/samples/python/batch_process.py b/samples/python/batch_process.py index e2615cf..bdf05f5 100644 --- a/samples/python/batch_process.py +++ b/samples/python/batch_process.py @@ -29,6 +29,7 @@ python batch_process.py ./documents ./converted png "*" """ +from enum import Enum from pathlib import Path from typing import Annotated @@ -37,8 +38,13 @@ from api.platform_api import PlatformAPIClient from helper_functions.document_helpers import validate_and_setup -# Supported output formats -SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx"] +class OutputFormat(str, Enum): + """Supported output formats for document conversion.""" + + PDF = "pdf" + DOCX = "docx" + XLSX = "xlsx" + PPTX = "pptx" app = typer.Typer() @@ -50,10 +56,8 @@ def main( ], output_folder: Annotated[Path, typer.Argument(help="Output folder for converted documents")], to_format: Annotated[ - str, - typer.Argument( - help=f"Target format for conversion. Supported: {', '.join(SUPPORTED_FORMATS)}" - ), + OutputFormat, + typer.Argument(help="Target format for conversion"), ], pattern: Annotated[ str, @@ -61,13 +65,6 @@ def main( ] = "*", ) -> None: """Process multiple documents in batch, converting them to a specified format.""" - # Validate output format - to_format_lower = to_format.lower() - if to_format_lower not in SUPPORTED_FORMATS: - typer.echo(f"❌ Error: Unsupported format '{to_format}'") - typer.echo(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - raise typer.Exit(code=1) - # Validate and setup with custom pattern files = validate_and_setup(input_folder, output_folder, file_patterns=[pattern]) typer.echo(f"📋 Found {len(files)} file(s) matching '{pattern}'\n") @@ -84,11 +81,11 @@ def main( try: # Convert to target format - typer.echo(f" 🔄 Converting to {to_format_lower.upper()}...") - converted = client.convert(file_path, to_format_lower) + typer.echo(f" 🔄 Converting to {to_format.value.upper()}...") + converted = client.convert(file_path, to_format.value) # Save converted file - output_file = output_folder / f"{file_path.stem}.{to_format_lower}" + output_file = output_folder / f"{file_path.stem}.{to_format.value}" output_file.write_bytes(converted) typer.echo(f" ✅ Converted: {output_file.name}\n") @@ -100,7 +97,7 @@ def main( # Display summary typer.echo("=" * 60) - typer.echo(f"✅ {success_count} file(s) converted to {to_format_lower.upper()}") + typer.echo(f"✅ {success_count} file(s) converted to {to_format.value.upper()}") if failed_count > 0: typer.echo(f"⚠️ {failed_count} file(s) FAILED to convert!") typer.echo(f"📂 Output: {output_folder.absolute()}") diff --git a/samples/python/convert_cli.py b/samples/python/convert_cli.py index 05cc5b0..18dae8f 100644 --- a/samples/python/convert_cli.py +++ b/samples/python/convert_cli.py @@ -28,6 +28,7 @@ python convert_cli.py spreadsheet.xlsx data.pdf pdf """ +from enum import Enum from pathlib import Path from typing import Annotated @@ -35,8 +36,14 @@ from api.platform_api import PlatformAPIClient -# Supported output formats -SUPPORTED_FORMATS = ["pdf", "docx", "xlsx", "pptx", "png"] +class OutputFormat(str, Enum): + """Supported output formats for document conversion.""" + + PDF = "pdf" + DOCX = "docx" + XLSX = "xlsx" + PPTX = "pptx" + PNG = "png" app = typer.Typer() @@ -47,25 +54,16 @@ def main( input_file: Annotated[Path, typer.Argument(help='Input file to convert')], output_file: Annotated[Path, typer.Argument(help='Output file path')], to_format: Annotated[ - str, - typer.Argument(help=f"Target format. Supported: {', '.join(SUPPORTED_FORMATS)}"), + OutputFormat, + typer.Argument(help="Target format for conversion"), ], ) -> None: """Convert a document from one format to another using the Platform API.""" - # Normalize format to lowercase - to_format = to_format.lower() - # Validate input file exists if not input_file.exists(): print(f'❌ Error: Input file not found: {input_file}') raise typer.Exit(code=1) - # Validate output format - if to_format not in SUPPORTED_FORMATS: - print(f"❌ Error: Unsupported format '{to_format}'") - print(f"Supported formats: {', '.join(SUPPORTED_FORMATS)}") - raise typer.Exit(code=1) - # Create output directory if needed output_file.parent.mkdir(parents=True, exist_ok=True) @@ -74,8 +72,8 @@ def main( try: # Convert document - print(f'🔄 Converting {input_file.name} to {to_format.upper()}...') - converted = client.convert(input_file, to_format) + print(f'🔄 Converting {input_file.name} to {to_format.value.upper()}...') + converted = client.convert(input_file, to_format.value) # Save converted file output_file.write_bytes(converted) diff --git a/samples/python/helper_functions/sign_helpers.py b/samples/python/helper_functions/sign_helpers.py index 0a31b84..22f47f8 100644 --- a/samples/python/helper_functions/sign_helpers.py +++ b/samples/python/helper_functions/sign_helpers.py @@ -125,8 +125,8 @@ def _upload_documents_to_envelope( token = sign_client.get_token() headers = {"Authorization": f"Bearer {token}"} - response = httpx.post( - f"{sign_client.settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", + response = sign_client._client.post( + f"{sign_client._settings.platform_base_url}/sign/envelopes/{envelope_id}/documents", headers=headers, files=files, ) From 402feb48cfe4857f21abeb73c817465e2612b23d Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Mon, 5 Jan 2026 00:29:15 +0000 Subject: [PATCH 18/19] initial commit with script in typescript --- samples/typescript/.env.example | 3 + samples/typescript/.gitignore | 30 ++ samples/typescript/README.md | 357 ++++++++++++++++++ samples/typescript/package.json | 38 ++ samples/typescript/src/api/base-client.ts | 104 +++++ samples/typescript/src/api/platform-api.ts | 234 ++++++++++++ samples/typescript/src/api/sign-api.ts | 280 ++++++++++++++ .../src/helpers/document-helpers.ts | 104 +++++ .../typescript/src/helpers/sign-helpers.ts | 320 ++++++++++++++++ .../typescript/src/scripts/batch-process.ts | 107 ++++++ .../src/scripts/bulk-password-protect.ts | 103 +++++ samples/typescript/src/scripts/convert-cli.ts | 91 +++++ .../src/scripts/employee-policy-onboarding.ts | 255 +++++++++++++ .../typescript/src/scripts/extract-data.ts | 118 ++++++ .../scripts/prepare-pdf-for-distribution.ts | 126 +++++++ samples/typescript/src/scripts/quickstart.ts | 86 +++++ .../src/scripts/redact-by-keyword.ts | 116 ++++++ .../src/scripts/smart-redact-pii.ts | 125 ++++++ samples/typescript/tsconfig.json | 20 + 19 files changed, 2617 insertions(+) create mode 100644 samples/typescript/.env.example create mode 100644 samples/typescript/.gitignore create mode 100644 samples/typescript/README.md create mode 100644 samples/typescript/package.json create mode 100644 samples/typescript/src/api/base-client.ts create mode 100644 samples/typescript/src/api/platform-api.ts create mode 100644 samples/typescript/src/api/sign-api.ts create mode 100644 samples/typescript/src/helpers/document-helpers.ts create mode 100644 samples/typescript/src/helpers/sign-helpers.ts create mode 100644 samples/typescript/src/scripts/batch-process.ts create mode 100644 samples/typescript/src/scripts/bulk-password-protect.ts create mode 100644 samples/typescript/src/scripts/convert-cli.ts create mode 100644 samples/typescript/src/scripts/employee-policy-onboarding.ts create mode 100644 samples/typescript/src/scripts/extract-data.ts create mode 100644 samples/typescript/src/scripts/prepare-pdf-for-distribution.ts create mode 100644 samples/typescript/src/scripts/quickstart.ts create mode 100644 samples/typescript/src/scripts/redact-by-keyword.ts create mode 100644 samples/typescript/src/scripts/smart-redact-pii.ts create mode 100644 samples/typescript/tsconfig.json diff --git a/samples/typescript/.env.example b/samples/typescript/.env.example new file mode 100644 index 0000000..3a3d066 --- /dev/null +++ b/samples/typescript/.env.example @@ -0,0 +1,3 @@ +PLATFORM_BASE_URL=https://api.gonitro.dev +PLATFORM_CLIENT_ID=your-client-id-here +PLATFORM_CLIENT_SECRET=your-client-secret-here diff --git a/samples/typescript/.gitignore b/samples/typescript/.gitignore new file mode 100644 index 0000000..4f46eb1 --- /dev/null +++ b/samples/typescript/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment files +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Output files +output/ +temp/ diff --git a/samples/typescript/README.md b/samples/typescript/README.md new file mode 100644 index 0000000..ac311b8 --- /dev/null +++ b/samples/typescript/README.md @@ -0,0 +1,357 @@ +# TypeScript Examples + +TypeScript examples for integrating with the Nitro Platform API. + +## Prerequisites + +- Node.js 18.0.0 or higher +- npm or yarn package manager + +## Setup + +1. **Install dependencies:** + ```bash + cd samples/typescript + npm install + ``` + +2. **Configure environment variables:** + ```bash + cp .env.example .env + # Edit .env with your credentials + ``` + +3. **Get your API credentials:** + - Go to https://admin.gonitro.com + - Navigate to Settings → API + - Click "Create Application" + - Name your application and save the Client ID and Client Secret + - Add these to your `.env` file: + ``` + PLATFORM_CLIENT_ID=your-client-id-here + PLATFORM_CLIENT_SECRET=your-client-secret-here + PLATFORM_BASE_URL=https://api.gonitro.dev + ``` + +4. **Test your setup:** + ```bash + npm run quickstart + ``` + +## Development + +### Running Scripts + +All scripts can be run using npm scripts or directly with tsx: + +```bash +# Using npm scripts (recommended) +npm run quickstart +npm run convert -- input.docx output.pdf pdf +npm run batch -- ./documents ./output pdf "*.docx" + +# Using tsx directly +tsx src/scripts/quickstart.ts +tsx src/scripts/convert-cli.ts input.docx output.pdf pdf +``` + +### Building the Project + +To compile TypeScript to JavaScript: + +```bash +npm run build +``` + +This will create compiled JavaScript files in the `dist/` directory. + +### Development Mode + +For development with auto-reloading: + +```bash +npm run dev +``` + +## Architecture + +### API Client Structure + +The TypeScript SDK follows a clean, object-oriented architecture similar to the Python SDK: + +``` +src/ +├── api/ +│ ├── base-client.ts # BaseOAuthClient - Shared OAuth2 authentication +│ ├── platform-api.ts # PlatformAPIClient - Document operations +│ └── sign-api.ts # SignAPIClient - eSignature operations +├── helpers/ +│ ├── document-helpers.ts # File validation and setup utilities +│ └── sign-helpers.ts # Sign workflow orchestration helpers +└── scripts/ + ├── quickstart.ts + ├── convert-cli.ts + ├── batch-process.ts + ├── extract-data.ts + ├── smart-redact-pii.ts + ├── employee-policy-onboarding.ts + ├── bulk-password-protect.ts + ├── redact-by-keyword.ts + └── prepare-pdf-for-distribution.ts +``` + +**BaseOAuthClient**: Base class providing OAuth2 authentication for all API clients +- Automatic token management with expiry buffer +- Connection pooling via axios +- Loads credentials from environment variables or .env file + +**PlatformAPIClient**: Client for document conversions, extractions, and transformations +- Inherits authentication from BaseOAuthClient +- Methods: `convert()`, `extractText()`, `detectPii()`, `redact()`, `compress()`, etc. +- Uses FormData for file uploads + +**SignAPIClient**: Client for eSignature/envelope operations +- Inherits authentication from BaseOAuthClient +- Methods: `createEnvelope()`, `createParticipant()`, `sendForSigning()`, etc. +- Full envelope lifecycle management + +## Available Scripts + +### Platform API Tools + +#### quickstart.ts +Test authentication and API connection. +```bash +npm run quickstart +``` + +#### convert-cli.ts +Convert a single document to another format. +```bash +npm run convert -- + +# Examples: +npm run convert -- document.docx document.pdf pdf +npm run convert -- presentation.pptx slide.png png +``` + +**Supported formats:** pdf, docx, xlsx, pptx, png + +#### batch-process.ts +Batch convert multiple documents in a folder. +```bash +npm run batch -- [pattern] + +# Examples: +npm run batch -- ./documents ./output pdf "*.docx" +npm run batch -- ./spreadsheets ./pdfs pdf "*.xlsx" +``` + +#### extract-data.ts +Extract structured data (forms or tables) from PDFs. +```bash +npm run extract -- + +# Examples: +npm run extract -- forms application.pdf data.json +npm run extract -- tables invoice.pdf tables.json +``` + +**Modes:** +- `forms` - Extract form fields (name-value pairs) +- `tables` - Extract table data (rows and columns) + +#### smart-redact-pii.ts +Automatically detect and redact personally identifiable information (PII). +```bash +npm run smart-redact -- + +# Example: +npm run smart-redact -- ./documents ./redacted +``` + +Detects and redacts: +- Social Security Numbers +- Phone numbers +- Email addresses +- Physical addresses +- Other PII patterns + +#### redact-by-keyword.ts +Redact specific keywords or phrases from PDFs. +```bash +npm run redact-keyword -- [keyword2 ...] + +# Examples: +npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" +npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" +``` + +#### bulk-password-protect.ts +Apply password protection to multiple PDFs. +```bash +npm run bulk-password -- + +# Example: +npm run bulk-password -- ./documents ./protected "MySecureP@ss123" +``` + +**Note:** Password must be at least 6 characters long. + +#### prepare-pdf-for-distribution.ts +Prepare documents for external distribution (convert to PDF, compress, remove metadata). +```bash +npm run prepare-pdf -- + +# Example: +npm run prepare-pdf -- ./brochures ./distribution +``` + +This script: +1. Converts documents to PDF format +2. Compresses PDFs to reduce file size +3. Removes all metadata properties (author, title, etc.) + +### Sign API Tools + +#### employee-policy-onboarding.ts +Complete HR onboarding workflow: send company policies to employees for signature. + +```bash +npm run employee-policy -- + +# Example: +npm run employee-policy -- ./policies ./employees.csv +``` + +**CSV Format:** +```csv +name,email +John Doe,john.doe@company.com +Jane Smith,jane.smith@company.com +``` + +**This script:** +1. Loads employee list from CSV +2. Loads policy documents from folder +3. For each employee: + - Creates a signature envelope + - Uploads all policy documents + - Adds the employee as a signer + - Configures signature fields + - Sends the envelope via email + - Monitors signing status (up to 60 minutes) + - Downloads signed documents when complete +4. Organizes output by employee folder + +**Output Structure:** +``` +output/ +├── john-doe/ +│ ├── signed-documents/ +│ │ ├── policy-1-signed.pdf +│ │ ├── policy-2-signed.pdf +│ │ └── audit-trail.pdf +│ └── envelope-info.json +├── jane-smith/ + ├── signed-documents/ + └── envelope-info.json +``` + +## Code Style & Conventions + +### Naming Conventions +- **Files:** kebab-case (e.g., `base-client.ts`, `convert-cli.ts`) +- **Classes:** PascalCase (e.g., `BaseOAuthClient`, `PlatformAPIClient`) +- **Functions/Variables:** camelCase (e.g., `getToken`, `convertDocument`) +- **Constants:** SCREAMING_SNAKE_CASE (e.g., `TOKEN_EXPIRY_BUFFER_SECONDS`) +- **Types/Interfaces:** PascalCase (e.g., `Settings`, `TokenResponse`) + +### Module System +This project uses ES Modules (ESM). All imports must use the `.js` extension: +```typescript +import { BaseOAuthClient } from './base-client.js'; +``` + +### Error Handling +Always use try-catch blocks and provide meaningful error messages: +```typescript +try { + const result = await client.convert(filePath, OutputFormat.PDF); + console.log('✅ Success'); +} catch (error: any) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); +} +``` + +## TypeScript Configuration + +The project uses strict TypeScript settings: +- Target: ES2022 +- Module: ESNext +- Strict mode enabled +- Source maps for debugging + +See `tsconfig.json` for complete configuration. + +## Differences from Python SDK + +While the TypeScript SDK maintains functional parity with the Python SDK, there are some differences: + +1. **Module System:** Uses ES Modules instead of Python imports +2. **Async/Await:** All API calls are asynchronous (using async/await) +3. **Type Safety:** Full TypeScript type checking +4. **CLI Framework:** Uses commander instead of typer +5. **File Operations:** Uses Node.js fs/promises instead of pathlib +6. **HTTP Client:** Uses axios instead of httpx + +## Dependencies + +### Production Dependencies +- **axios** - HTTP client with connection pooling +- **commander** - CLI framework +- **dotenv** - Environment variable management +- **form-data** - Multipart form data for file uploads +- **csv-parse** - CSV file parsing +- **adm-zip** - ZIP file extraction +- **mime-types** - MIME type detection + +### Development Dependencies +- **typescript** - TypeScript compiler +- **tsx** - TypeScript execution and development +- **@types/node** - Node.js type definitions + +## Troubleshooting + +### "Missing required environment variables" +Make sure you've created a `.env` file with your API credentials. Copy `.env.example` and fill in your values. + +### "ENOENT: no such file or directory" +Check that your input file paths are correct and the files exist. + +### "401 Unauthorized" +Your API credentials may be incorrect or expired. Double-check your `PLATFORM_CLIENT_ID` and `PLATFORM_CLIENT_SECRET` in the `.env` file. + +### "Network error" or timeout +Check your internet connection and verify that `PLATFORM_BASE_URL` is correct in your `.env` file. + +### TypeScript compilation errors +Make sure all dependencies are installed: +```bash +npm install +``` + +## Further Resources + +- [Nitro Platform API Documentation](https://developers.gonitro.com/docs) +- [Nitro Admin Portal](https://admin.gonitro.com) +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [Node.js Documentation](https://nodejs.org/docs/) + +## Support + +For questions or issues: +- Check the [Python SDK examples](../python/README.md) for additional context +- Review the [API documentation](https://developers.gonitro.com/docs) +- Contact Nitro support through the admin portal diff --git a/samples/typescript/package.json b/samples/typescript/package.json new file mode 100644 index 0000000..d70caca --- /dev/null +++ b/samples/typescript/package.json @@ -0,0 +1,38 @@ +{ + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "type": "module", + "description": "TypeScript sample scripts for the Nitro Platform API", + "scripts": { + "build": "tsc", + "dev": "tsx watch", + "quickstart": "tsx src/scripts/quickstart.ts", + "convert": "tsx src/scripts/convert-cli.ts", + "batch": "tsx src/scripts/batch-process.ts", + "extract": "tsx src/scripts/extract-data.ts", + "smart-redact": "tsx src/scripts/smart-redact-pii.ts", + "employee-policy": "tsx src/scripts/employee-policy-onboarding.ts", + "bulk-password": "tsx src/scripts/bulk-password-protect.ts", + "redact-keyword": "tsx src/scripts/redact-by-keyword.ts", + "prepare-pdf": "tsx src/scripts/prepare-pdf-for-distribution.ts" + }, + "dependencies": { + "axios": "^1.6.0", + "commander": "^11.0.0", + "dotenv": "^16.0.0", + "form-data": "^4.0.0", + "csv-parse": "^5.5.0", + "adm-zip": "^0.5.10", + "mime-types": "^2.1.35" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/adm-zip": "^0.5.5", + "@types/mime-types": "^2.1.4", + "typescript": "^5.3.0", + "tsx": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/samples/typescript/src/api/base-client.ts b/samples/typescript/src/api/base-client.ts new file mode 100644 index 0000000..2c40168 --- /dev/null +++ b/samples/typescript/src/api/base-client.ts @@ -0,0 +1,104 @@ +/** + * Base client for API authentication with OAuth2. + */ + +import axios, { AxiosInstance } from 'axios'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); + +/** + * Token expiry buffer in seconds to account for clock skew and network latency. + */ +const TOKEN_EXPIRY_BUFFER_SECONDS = 60; + +/** + * OAuth2 token response model. + */ +interface TokenResponse { + accessToken: string; + expiresIn: number; +} + +/** + * Application settings for API authentication. + */ +interface Settings { + platformClientId: string; + platformClientSecret: string; + platformBaseUrl: string; +} + +/** + * Base class for API clients with OAuth2 authentication. + */ +export class BaseOAuthClient { + protected settings: Settings; + private token: string | null = null; + private tokenExpiry: number = 0; + protected client: AxiosInstance; + + /** + * Creates a new BaseOAuthClient instance. + * @param settings - Optional settings override. If not provided, loads from environment variables. + */ + constructor(settings?: Settings) { + this.settings = settings || this.loadSettingsFromEnv(); + + // Create axios instance with connection pooling + this.client = axios.create({ + timeout: 30000, + maxRedirects: 5, + }); + } + + /** + * Load settings from environment variables. + * @returns Settings object populated from environment. + * @throws Error if required environment variables are missing. + */ + private loadSettingsFromEnv(): Settings { + const clientId = process.env.PLATFORM_CLIENT_ID; + const clientSecret = process.env.PLATFORM_CLIENT_SECRET; + const baseUrl = process.env.PLATFORM_BASE_URL || 'https://api.gonitro.dev'; + + if (!clientId || !clientSecret) { + throw new Error( + 'Missing required environment variables: PLATFORM_CLIENT_ID and PLATFORM_CLIENT_SECRET must be set' + ); + } + + return { + platformClientId: clientId, + platformClientSecret: clientSecret, + platformBaseUrl: baseUrl, + }; + } + + /** + * Get or refresh OAuth2 access token. + * @returns OAuth2 access token for API authentication. + */ + protected async getToken(): Promise { + // Return cached token if still valid + if (this.token && Date.now() / 1000 < this.tokenExpiry) { + return this.token; + } + + // Request new token + const response = await this.client.post( + `${this.settings.platformBaseUrl}/oauth/token`, + { + clientID: this.settings.platformClientId, + clientSecret: this.settings.platformClientSecret, + } + ); + + const tokenData = response.data; + this.token = tokenData.accessToken; + this.tokenExpiry = Date.now() / 1000 + tokenData.expiresIn - TOKEN_EXPIRY_BUFFER_SECONDS; + + return tokenData.accessToken; + } +} diff --git a/samples/typescript/src/api/platform-api.ts b/samples/typescript/src/api/platform-api.ts new file mode 100644 index 0000000..71d2c97 --- /dev/null +++ b/samples/typescript/src/api/platform-api.ts @@ -0,0 +1,234 @@ +/** + * Platform API client for Nitro Platform integrations. + */ + +import { readFile } from 'fs/promises'; +import FormData from 'form-data'; +import axios from 'axios'; +import { BaseOAuthClient } from './base-client.js'; +import { lookup } from 'mime-types'; + +/** + * Supported output formats for document conversion. + */ +export enum OutputFormat { + PDF = 'pdf', + DOCX = 'docx', + XLSX = 'xlsx', + PPTX = 'pptx', + PNG = 'png', +} + +/** + * Type for API endpoints. + */ +type Endpoint = 'conversions' | 'extractions' | 'transformations'; + +/** + * Synchronous client for Nitro Platform API operations. + */ +export class PlatformAPIClient extends BaseOAuthClient { + /** + * Make API request with file upload. + * @param endpoint - API endpoint (conversions, extractions, transformations). + * @param method - API method to call. + * @param filePath - Path to file to upload. + * @param params - Optional parameters for the method. + * @returns Parsed JSON response. + */ + private async request( + endpoint: Endpoint, + method: string, + filePath: string, + params?: Record + ): Promise { + const token = await this.getToken(); + + // Read file and detect MIME type + const fileBuffer = await readFile(filePath); + const fileName = filePath.split('/').pop() || 'file'; + const mimeType = lookup(filePath) || 'application/octet-stream'; + + // Create form data + const form = new FormData(); + form.append('file', fileBuffer, { + filename: fileName, + contentType: mimeType, + }); + form.append('method', method); + form.append('params', JSON.stringify(params || {})); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/${endpoint}`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + } + ); + + return response.data; + } + + /** + * Make API request and return raw bytes from result file URL. + * @param endpoint - API endpoint. + * @param method - API method to call. + * @param filePath - Path to file to upload. + * @param params - Optional parameters for the method. + * @returns File content as Buffer. + */ + private async requestBytes( + endpoint: Endpoint, + method: string, + filePath: string, + params?: Record + ): Promise { + const result = await this.request(endpoint, method, filePath, params); + + // Download from S3 URL + const downloadUrl = result.result.file.URL; + const downloadResponse = await axios.get(downloadUrl, { + responseType: 'arraybuffer', + }); + + return Buffer.from(downloadResponse.data); + } + + /** + * Convert document to specified format. + * @param filePath - Path to input file. + * @param toFormat - Target format (pdf, docx, xlsx, pptx, png). + * @returns Converted file as Buffer. + */ + async convert(filePath: string, toFormat: OutputFormat): Promise { + return this.requestBytes('conversions', 'convert', filePath, { to: toFormat }); + } + + /** + * Extract text from document. + * @param filePath - Path to input file. + * @returns Extracted text data. + */ + async extractText(filePath: string): Promise { + return this.request('extractions', 'extract-text', filePath); + } + + /** + * Extract form data from PDF. + * @param filePath - Path to PDF file. + * @returns Extracted form data. + */ + async extractForms(filePath: string): Promise { + return this.request('extractions', 'extract-forms', filePath); + } + + /** + * Extract table data from PDF. + * @param filePath - Path to PDF file. + * @returns Extracted table data. + */ + async extractTables(filePath: string): Promise { + return this.request('extractions', 'extract-tables', filePath); + } + + /** + * Detect PII and return bounding boxes. + * @param filePath - Path to input file. + * @param language - Language code (default: 'en'). + * @returns PII detection results with bounding boxes. + */ + async detectPii(filePath: string, language: string = 'en'): Promise { + return this.request('extractions', 'extract-pii-bounding-boxes', filePath, { language }); + } + + /** + * Find bounding boxes for specified text strings. + * @param filePath - Path to input file. + * @param texts - Array of text strings to search for. + * @returns Bounding boxes for found text. + */ + async findTextBoxes(filePath: string, texts: string[]): Promise { + return this.request('extractions', 'extract-text-bounding-boxes', filePath, { texts }); + } + + /** + * Redact specified bounding boxes. + * @param filePath - Path to input file. + * @param redactions - Array of redaction specifications. + * @returns Redacted file as Buffer. + */ + async redact(filePath: string, redactions: any[]): Promise { + return this.requestBytes('transformations', 'redact', filePath, { redactions }); + } + + /** + * Add password protection to PDF. + * @param filePath - Path to PDF file. + * @param password - Password to set. + * @returns Protected PDF as Buffer. + */ + async passwordProtect(filePath: string, password: string): Promise { + return this.requestBytes('transformations', 'protect', filePath, { + ownerPassword: password, + userPassword: password, + }); + } + + /** + * Compress PDF. + * @param filePath - Path to PDF file. + * @param level - Compression level (1-3, default: 2). + * @returns Compressed PDF as Buffer. + */ + async compress(filePath: string, level: number = 2): Promise { + return this.requestBytes('transformations', 'compress', filePath, { level }); + } + + /** + * Set or clear PDF metadata properties. + * @param filePath - Path to PDF file. + * @param properties - Object with property names and values. + * @returns Modified PDF as Buffer. + */ + async setProperties(filePath: string, properties: Record): Promise { + return this.requestBytes('transformations', 'set-properties', filePath, properties); + } + + /** + * Merge multiple PDFs. + * @param filePaths - Array of paths to PDF files to merge. + * @returns Merged PDF as Buffer. + */ + async merge(filePaths: string[]): Promise { + const token = await this.getToken(); + + // Create form data with multiple files + const form = new FormData(); + + for (const filePath of filePaths) { + const fileBuffer = await readFile(filePath); + const fileName = filePath.split('/').pop() || 'file'; + form.append('file', fileBuffer, { filename: fileName }); + } + + form.append('method', 'merge'); + form.append('params', '{}'); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/transformations`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + responseType: 'arraybuffer', + } + ); + + return Buffer.from(response.data); + } +} diff --git a/samples/typescript/src/api/sign-api.ts b/samples/typescript/src/api/sign-api.ts new file mode 100644 index 0000000..2925e41 --- /dev/null +++ b/samples/typescript/src/api/sign-api.ts @@ -0,0 +1,280 @@ +/** + * Sign API client for Nitro Sign integrations (eSignature operations). + */ + +import { readFile } from 'fs/promises'; +import FormData from 'form-data'; +import { BaseOAuthClient } from './base-client.js'; + +/** + * Synchronous client for Nitro Sign API operations (eSignature/envelopes). + */ +export class SignAPIClient extends BaseOAuthClient { + /** + * Make authenticated API request returning JSON. + * @param method - HTTP method. + * @param endpoint - API endpoint path. + * @param jsonData - Optional JSON payload. + * @param params - Optional query parameters. + * @returns Response data. + */ + private async request( + method: string, + endpoint: string, + jsonData?: Record, + params?: Record + ): Promise { + const token = await this.getToken(); + + try { + const response = await this.client.request({ + method, + url: `${this.settings.platformBaseUrl}${endpoint}`, + headers: { + Authorization: `Bearer ${token}`, + }, + data: jsonData, + params, + }); + + return response.data; + } catch (error: any) { + // Try to get error details from response + if (error.response?.data) { + console.error(` ❌ API Error Response:`, error.response.data); + } else if (error.response?.text) { + console.error(` ❌ API Error (no JSON):`, error.response.text); + } + throw error; + } + } + + /** + * Make authenticated API request returning binary data. + * @param method - HTTP method. + * @param endpoint - API endpoint path. + * @param params - Optional query parameters. + * @returns Binary response data as Buffer. + */ + private async requestBytes( + method: string, + endpoint: string, + params?: Record + ): Promise { + const token = await this.getToken(); + + const response = await this.client.request({ + method, + url: `${this.settings.platformBaseUrl}${endpoint}`, + headers: { + Authorization: `Bearer ${token}`, + }, + params, + responseType: 'arraybuffer', + }); + + return Buffer.from(response.data); + } + + // ========== Envelope Management ========== + + /** + * List all envelopes with cursor-based pagination. + * @param pageAfter - Cursor token to get items after the last item from previous response. + * @param pageBefore - Cursor token to get items before the last item from previous response. + * @returns Object with 'items' (list of envelopes) and optional 'nextPage' (cursor token). + */ + async listEnvelopes(pageAfter?: string, pageBefore?: string): Promise { + const params: Record = {}; + if (pageAfter) { + params.pageAfter = pageAfter; + } else if (pageBefore) { + params.pageBefore = pageBefore; + } + + return this.request('GET', '/sign/envelopes', undefined, params); + } + + /** + * Create a new envelope. + * @param envelopeData - Envelope configuration including name, documents, participants, fields. + * @returns Created envelope with ID and status. + */ + async createEnvelope(envelopeData: Record): Promise { + return this.request('POST', '/sign/envelopes', envelopeData); + } + + /** + * Get envelope details by ID. + * @param envelopeId - UUID of the envelope. + * @returns Envelope details including status, participants, documents. + */ + async getEnvelope(envelopeId: string): Promise { + return this.request('GET', `/sign/envelopes/${envelopeId}`); + } + + /** + * Update an envelope. + * @param envelopeId - UUID of the envelope. + * @param updates - Fields to update (name, participants, etc.). + * @returns Updated envelope data. + */ + async updateEnvelope(envelopeId: string, updates: Record): Promise { + return this.request('PATCH', `/sign/envelopes/${envelopeId}`, updates); + } + + /** + * Delete an envelope by ID. + * @param envelopeId - UUID of the envelope. + */ + async deleteEnvelope(envelopeId: string): Promise { + const token = await this.getToken(); + + await this.client.delete(`${this.settings.platformBaseUrl}/sign/envelopes/${envelopeId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + } + + // ========== Document Management ========== + + /** + * Upload a document to an envelope using form-data. + * @param envelopeId - ID of the envelope. + * @param filePath - Path to the PDF file to upload. + * @param documentName - Optional custom name for the document. + * @returns Created document with ID. + */ + async createDocument( + envelopeId: string, + filePath: string, + documentName?: string + ): Promise { + const token = await this.getToken(); + + if (!documentName) { + documentName = filePath.split('/').pop() || 'document.pdf'; + } + + // Read the binary content of the PDF file + const pdfBinary = await readFile(filePath); + + // Prepare metadata as JSON string + const metadata = JSON.stringify({ name: documentName }); + + // Prepare form-data with binary content + const form = new FormData(); + form.append('metadata', metadata, { + filename: 'metadata', + contentType: 'application/json', + }); + form.append('payload', pdfBinary, { + filename: filePath.split('/').pop() || 'document.pdf', + contentType: 'application/pdf', + }); + + const response = await this.client.post( + `${this.settings.platformBaseUrl}/sign/envelopes/${envelopeId}/documents`, + form, + { + headers: { + Authorization: `Bearer ${token}`, + ...form.getHeaders(), + }, + } + ); + + return response.data; + } + + // ========== Participant Management ========== + + /** + * Add a participant to an envelope. + * @param envelopeId - ID of the envelope. + * @param participantData - Participant configuration with role, email, name. + * @returns Created participant with ID. + */ + async createParticipant( + envelopeId: string, + participantData: Record + ): Promise { + return this.request( + 'POST', + `/sign/envelopes/${envelopeId}/participants`, + participantData + ); + } + + // ========== Field Management ========== + + /** + * Add a signature field to a document in an envelope. + * @param envelopeId - ID of the envelope. + * @param documentId - ID of the document. + * @param fieldData - Field configuration with boundingBox, participantID, type, page. + * @returns Created field with ID. + */ + async createField( + envelopeId: string, + documentId: string, + fieldData: Record + ): Promise { + return this.request( + 'POST', + `/sign/envelopes/${envelopeId}/documents/${documentId}/fields`, + fieldData + ); + } + + // ========== Envelope Actions ========== + + /** + * Send envelope to participants for signing. + * This transitions the envelope from 'drafted' to 'sent' status. + * @param envelopeId - UUID of the envelope. + * @returns Updated envelope with 'sent' status. + */ + async sendForSigning(envelopeId: string): Promise { + return this.request('POST', `/sign/envelopes/${envelopeId}:send-for-signing`); + } + + /** + * Cancel an envelope that was sent for signing. + * @param envelopeId - UUID of the envelope. + * @returns Envelope with 'cancelled' status. + */ + async cancelEnvelope(envelopeId: string): Promise { + return this.request('PUT', `/sign/envelopes/${envelopeId}/cancel`); + } + + /** + * Send reminder notifications to pending signers. + * @param envelopeId - UUID of the envelope. + * @returns Confirmation of reminder sent. + */ + async sendReminders(envelopeId: string): Promise { + return this.request('POST', `/sign/envelopes/${envelopeId}/reminders`); + } + + // ========== Document Downloads ========== + + /** + * Download the sealed (signed and completed) envelope. + * @param envelopeId - UUID of the envelope. + * @returns PDF bytes of the sealed document. + */ + async downloadSealedEnvelope(envelopeId: string): Promise { + return this.requestBytes('GET', `/sign/envelopes/${envelopeId}:download-sealed`); + } + + /** + * Download the original (unsigned) envelope documents. + * @param envelopeId - UUID of the envelope. + * @returns Original document bytes. + */ + async downloadOriginalEnvelope(envelopeId: string): Promise { + return this.requestBytes('GET', `/sign/envelopes/${envelopeId}:download-original`); + } +} diff --git a/samples/typescript/src/helpers/document-helpers.ts b/samples/typescript/src/helpers/document-helpers.ts new file mode 100644 index 0000000..db8d131 --- /dev/null +++ b/samples/typescript/src/helpers/document-helpers.ts @@ -0,0 +1,104 @@ +/** + * Common helper utilities for document processing scripts. + */ + +import { readdir, mkdir, stat } from 'fs/promises'; +import { join } from 'path'; + +/** + * Check if a filename matches any of the given glob patterns. + * @param filename - Name of the file to check. + * @param patterns - Array of glob patterns (e.g., ['*.docx', '*.pdf']). + * @returns True if filename matches any pattern. + */ +function matchesPattern(filename: string, patterns: string[]): boolean { + for (const pattern of patterns) { + // Convert simple glob pattern to regex + // This handles basic * wildcard matching + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`, 'i'); + + if (regex.test(filename)) { + return true; + } + } + return false; +} + +/** + * Recursively find files matching patterns in a directory. + * @param dir - Directory to search. + * @param patterns - Array of glob patterns. + * @param files - Accumulator for found files (used internally). + * @returns Array of file paths. + */ +async function findMatchingFiles( + dir: string, + patterns: string[], + files: string[] = [] +): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + if (entry.isDirectory()) { + // Recursively search subdirectories + await findMatchingFiles(fullPath, patterns, files); + } else if (entry.isFile() && matchesPattern(entry.name, patterns)) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Validate input folder and setup output folder. Returns list of files to process. + * @param inputFolder - Path to the input directory containing files to process. + * @param outputFolder - Path to the output directory for processed files. + * @param filePatterns - List of glob patterns to match files (e.g., ['*.docx', '*.pdf']). + * If not provided, defaults to common Office document formats. + * @returns Array of file paths for files matching the patterns. + * @throws Error if input folder is invalid or no matching files are found. + */ +export async function validateAndSetup( + inputFolder: string, + outputFolder: string, + filePatterns?: string[] +): Promise { + // Default patterns for Office documents if none provided + const patterns = filePatterns || ['*.docx', '*.doc', '*.xlsx', '*.xls', '*.pptx', '*.ppt']; + + // Validate input folder exists + try { + const inputStat = await stat(inputFolder); + if (!inputStat.isDirectory()) { + console.error(`❌ Error: Invalid input folder: ${inputFolder}`); + process.exit(1); + } + } catch (error) { + console.error(`❌ Error: Invalid input folder: ${inputFolder}`); + process.exit(1); + } + + // Create output folder if needed + try { + await mkdir(outputFolder, { recursive: true }); + } catch (error) { + console.error(`❌ Error: Could not create output folder: ${outputFolder}`); + process.exit(1); + } + + // Find all matching files + const files = await findMatchingFiles(inputFolder, patterns); + + if (files.length === 0) { + console.error(`❌ No files matching patterns ${patterns.join(', ')} found in ${inputFolder}`); + process.exit(1); + } + + return files; +} diff --git a/samples/typescript/src/helpers/sign-helpers.ts b/samples/typescript/src/helpers/sign-helpers.ts new file mode 100644 index 0000000..b657dc9 --- /dev/null +++ b/samples/typescript/src/helpers/sign-helpers.ts @@ -0,0 +1,320 @@ +/** + * Sign API helper utilities for envelope operations. + */ + +import { readFile, readdir, mkdir, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { parse } from 'csv-parse/sync'; +import AdmZip from 'adm-zip'; +import { SignAPIClient } from '../api/sign-api.js'; + +/** + * Convert employee name to folder-safe name. + * @param employeeName - Full name like "John Doe". + * @returns Folder-safe name like "john-doe". + */ +export function createEmployeeFolderName(employeeName: string): string { + return employeeName.toLowerCase().replace(/\s+/g, '-').replace(/\./g, ''); +} + +/** + * Load employee list from CSV file. + * @param csvPath - Path to CSV file with columns: name, email. + * @returns Array of employee objects with 'name' and 'email'. + * @throws Error if CSV format is invalid. + */ +export async function loadEmployeesFromCsv(csvPath: string): Promise> { + try { + const content = await readFile(csvPath, 'utf-8'); + const records = parse(content, { + columns: true, + skip_empty_lines: true, + trim: true, + }); + + const employees: Array<{ name: string; email: string }> = []; + + for (const row of records) { + const name = row.name?.trim(); + const email = row.email?.trim(); + + if (name && email && email.includes('@')) { + employees.push({ name, email }); + } + } + + if (employees.length === 0) { + throw new Error('No valid employees found in CSV'); + } + + return employees; + } catch (error: any) { + throw new Error(`Failed to load employees from CSV: ${error.message}`); + } +} + +/** + * Load all PDF files from the policies folder. + * @param policiesFolder - Path to folder containing policy PDFs. + * @returns Array of document objects with 'name', 'binary', and 'path'. + */ +export async function loadPolicyDocumentsFromFolder(policiesFolder: string): Promise> { + const files = await readdir(policiesFolder); + const pdfFiles = files.filter(f => f.toLowerCase().endsWith('.pdf')); + + if (pdfFiles.length === 0) { + throw new Error(`No PDF files found in ${policiesFolder}`); + } + + console.log(`📂 Found ${pdfFiles.length} policy document(s)\n`); + + const documents: Array<{ name: string; binary: Buffer; path: string }> = []; + + for (const filename of pdfFiles) { + const filePath = join(policiesFolder, filename); + const binary = await readFile(filePath); + + documents.push({ + name: filename, + binary, + path: filePath, + }); + } + + return documents; +} + +/** + * Upload documents to an existing envelope. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of the envelope to upload to. + * @param documents - Array of document objects with 'name', 'binary', 'path'. + * @returns Array of document IDs. + */ +async function uploadDocumentsToEnvelope( + signClient: SignAPIClient, + envelopeId: string, + documents: Array<{ name: string; binary: Buffer; path: string }> +): Promise { + const documentIds: string[] = []; + + for (const doc of documents) { + // Use the createDocument method from SignAPIClient + // We need to save to a temp file since createDocument expects a file path + // For simplicity, we'll use the existing path + const document = await signClient.createDocument(envelopeId, doc.path, doc.name); + documentIds.push(document.ID); + } + + return documentIds; +} + +/** + * Create envelope and upload documents. + * @param signClient - Sign API client instance. + * @param documents - Array of document objects. + * @param employeeName - Full name of employee. + * @param employeeEmail - Email address of employee. + * @returns Tuple of [envelopeId, documentIds]. + */ +export async function createSignatureEnvelope( + signClient: SignAPIClient, + documents: Array<{ name: string; binary: Buffer; path: string }>, + employeeName: string, + employeeEmail: string +): Promise<[string, string[]]> { + // Create empty envelope + const envelopeData = { + name: `Company Policies - ${employeeName}`, + mode: 'parallel', + notification: { + subject: 'Please sign: Company Policies', + body: `Hello ${employeeName}, please review and sign the attached company policy documents.`, + }, + }; + + const envelope = await signClient.createEnvelope(envelopeData); + const envelopeId = envelope.ID; + + // Upload documents to envelope + const documentIds = await uploadDocumentsToEnvelope(signClient, envelopeId, documents); + + return [envelopeId, documentIds]; +} + +/** + * Add signature and date fields to all documents in envelope. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of the envelope. + * @param documentIds - Array of document IDs to add fields to. + * @param participantId - ID of the participant who will sign. + */ +export async function addSignatureFieldsToDocuments( + signClient: SignAPIClient, + envelopeId: string, + documentIds: string[], + participantId: string +): Promise { + for (const docId of documentIds) { + // Add signature field (positioned bottom-left of page) + // Coordinates: [x, y, width, height] in points (72 points = 1 inch) + // Standard letter page: 612 x 792 points + const signatureFieldData = { + participantID: participantId, + type: 'signature', + label: 'Your Signature', + page: 1, + boundingBox: [50, 100, 200, 50], // Bottom area, safe coordinates + required: true, + }; + await signClient.createField(envelopeId, docId, signatureFieldData); + + // Add date field (positioned to the right of signature) + const dateFieldData = { + participantID: participantId, + type: 'date', + label: 'Date Signed', + page: 1, + boundingBox: [270, 100, 150, 50], // Next to signature, safe coordinates + required: true, + format: 'MM/DD/YYYY', + }; + await signClient.createField(envelopeId, docId, dateFieldData); + } +} + +/** + * Send envelope and monitor until signed or timeout. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of envelope to send. + * @param email - Email address of recipient. + * @param timeoutMinutes - Maximum time to wait (default: 60). + * @returns Final status: 'sealed', 'cancelled', 'timeout', or 'error'. + */ +export async function sendAndMonitorEnvelope( + signClient: SignAPIClient, + envelopeId: string, + email: string, + timeoutMinutes: number = 60 +): Promise { + // Send envelope + await signClient.sendForSigning(envelopeId); + + // Log send time and status + const sendTime = new Date().toISOString().replace('T', ' ').substring(0, 19); + console.log(` ✅ Sent at: ${sendTime}`); + console.log(` 📧 Email sent to: ${email}`); + console.log(` 🔗 Envelope ID: ${envelopeId}`); + + // Check initial status + const envelope = await signClient.getEnvelope(envelopeId); + console.log(` 📊 Status: ${envelope.status}`); + + // Monitor for completion + console.log(' ⏳ Waiting for signature...'); + console.log(` ⏱️ Checking every 30 seconds (timeout: ${timeoutMinutes} minutes)`); + + const status = await monitorEnvelope(signClient, envelopeId, timeoutMinutes); + + // Log final status + const completionTime = new Date().toISOString().replace('T', ' ').substring(0, 19); + console.log(` 📊 Final status: ${status}`); + console.log(` 🕐 Completed at: ${completionTime}`); + + return status; +} + +/** + * Monitor envelope until signed, cancelled, or timeout. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of envelope to monitor. + * @param timeoutMinutes - Maximum time to wait (default: 60). + * @returns Final status: 'sealed', 'cancelled', 'timeout', or 'error'. + */ +export async function monitorEnvelope( + signClient: SignAPIClient, + envelopeId: string, + timeoutMinutes: number = 60 +): Promise { + const checkInterval = 30; // seconds + const maxChecks = Math.floor((timeoutMinutes * 60) / checkInterval); + + for (let i = 0; i < maxChecks; i++) { + try { + const envelope = await signClient.getEnvelope(envelopeId); + const status = envelope.status; + + if (status === 'sealed') { + return 'sealed'; + } + if (['cancelled', 'rejected', 'deleted'].includes(status)) { + return 'cancelled'; + } + + if (i < maxChecks - 1) { + await new Promise(resolve => setTimeout(resolve, checkInterval * 1000)); + } + } catch (error: any) { + console.error(` ⚠️ Error checking status: ${error.message}`); + return 'error'; + } + } + + return 'timeout'; +} + +/** + * Download sealed envelope and extract signed documents. + * @param signClient - Sign API client instance. + * @param envelopeId - ID of sealed envelope. + * @param outputFolder - Employee-specific output folder. + * @param documentName - Name for the saved file (should end with .zip). + * @returns Path to extracted documents folder. + */ +export async function downloadSignedDocument( + signClient: SignAPIClient, + envelopeId: string, + outputFolder: string, + documentName: string +): Promise { + // Download sealed envelope (returns ZIP file) + const zipBytes = await signClient.downloadSealedEnvelope(envelopeId); + + // Save as temporary ZIP file + if (!documentName.endsWith('.zip')) { + documentName = documentName.replace('.pdf', '.zip'); + } + + const tempZipPath = join(outputFolder, documentName); + await writeFile(tempZipPath, zipBytes); + + // Extract the ZIP contents + const extractFolder = join(outputFolder, 'signed-documents'); + await mkdir(extractFolder, { recursive: true }); + + const zip = new AdmZip(tempZipPath); + zip.extractAllTo(extractFolder, true); + + // Delete the ZIP file after extraction + const fs = await import('fs/promises'); + await fs.unlink(tempZipPath); + + // Save envelope metadata + const envelope = await signClient.getEnvelope(envelopeId); + const jsonPath = join(outputFolder, 'envelope-info.json'); + await writeFile(jsonPath, JSON.stringify(envelope, null, 2)); + + console.log(` 💾 Extracted to: ${extractFolder}`); + + return extractFolder; +} + +/** + * Log a processing step with consistent formatting. + * @param message - The message to log. + * @param emoji - Optional emoji to prepend (default: none). + */ +export function logStep(message: string, emoji?: string): void { + const prefix = emoji ? `${emoji} ` : ' '; + console.log(`${prefix}${message}`); +} diff --git a/samples/typescript/src/scripts/batch-process.ts b/samples/typescript/src/scripts/batch-process.ts new file mode 100644 index 0000000..477bf08 --- /dev/null +++ b/samples/typescript/src/scripts/batch-process.ts @@ -0,0 +1,107 @@ +#!/usr/bin/env node +/** + * 📁 BATCH DOCUMENT CONVERSION + * ============================= + * + * The script exemplifies a typical workflow for document format standardization. + * As an IT administrator, it's essential to convert large collections of documents + * into standardized formats for archival, compliance, or system integration purposes. + * Manually converting individual files through desktop applications is time-consuming + * and impractical for large document sets, often leading to inconsistent results and + * wasted effort. + * + * This workflow automates bulk document conversion. The script processes files + * individually - for every document matching the specified pattern in the input folder, + * it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using + * high-fidelity conversion algorithms. Each converted file is saved to the output + * folder with the same base name but the new format extension, resulting in a + * complete batch of standardized documents. + * + * BATCH CONVERSION FEATURES: + * ✓ Multiple format support (PDF, DOCX, XLSX, PNG, JPG, etc.) + * ✓ Flexible file pattern matching (*.docx, *.xlsx, *.pptx, *.pdf.) + * + * USAGE: + * npm run batch -- [pattern] + * # or with tsx: + * tsx src/scripts/batch-process.ts [pattern] + * + * EXAMPLES: + * npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" + * npm run batch -- ./documents ./converted png "*" + */ + +import { program } from 'commander'; +import { writeFile } from 'fs/promises'; +import { join, parse } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('batch-process') + .description('Process multiple documents in batch, converting them to a specified format') + .argument('', 'Input folder containing documents to convert') + .argument('', 'Output folder for converted documents') + .argument('', 'Target format for conversion (pdf, docx, xlsx, pptx)') + .argument('[pattern]', 'File pattern to match (e.g., "*.docx", "*.pdf", "*")', '*') + .action(async (inputFolder: string, outputFolder: string, format: string, pattern: string) => { + try { + // Validate format + const validFormats = Object.values(OutputFormat); + if (!validFormats.includes(format.toLowerCase() as OutputFormat)) { + console.error(`❌ Error: Invalid format '${format}'. Valid formats: ${validFormats.join(', ')}`); + process.exit(1); + } + + const toFormat = format.toLowerCase() as OutputFormat; + + // Validate and setup with custom pattern + const files = await validateAndSetup(inputFolder, outputFolder, [pattern]); + console.log(`📋 Found ${files.length} file(s) matching '${pattern}'\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const filePath = files[i]; + const fileName = filePath.split('/').pop() || filePath; + const fileBaseName = parse(filePath).name; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Convert to target format + console.log(` 🔄 Converting to ${toFormat.toUpperCase()}...`); + const converted = await client.convert(filePath, toFormat); + + // Save converted file + const outputFile = join(outputFolder, `${fileBaseName}.${toFormat}`); + await writeFile(outputFile, converted); + + console.log(` ✅ Converted: ${parse(outputFile).base}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} file(s) converted to ${toFormat.toUpperCase()}`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} file(s) FAILED to convert!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Batch processing FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/bulk-password-protect.ts b/samples/typescript/src/scripts/bulk-password-protect.ts new file mode 100644 index 0000000..0e05ed4 --- /dev/null +++ b/samples/typescript/src/scripts/bulk-password-protect.ts @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/** + * 🔐 BULK PASSWORD PROTECTION + * ============================ + * + * The script exemplifies a typical workflow for securing confidential documents. + * As a security professional, it's essential to protect sensitive documents with + * passwords before distributing them to authorized personnel, storing them in shared + * drives, or archiving them for compliance purposes. Manually setting passwords on + * individual files is tedious and inconsistent, leading to weak passwords or missed + * files that remain unprotected. + * + * This workflow automates secure document protection. The script processes each PDF + * file individually - for every document in the input folder, it applies robust + * password encryption using a consistent password across all files. Each protected + * file is saved to the output folder with the same filename, ensuring that the entire + * batch of documents maintains uniform security standards. The result is a complete + * set of password-protected PDFs ready for secure distribution or storage. + * + * DOCUMENT SECURITY STANDARDS: + * ✓ Password encryption (AES-256) + * ✓ Batch processing (entire folders) + * ✓ Consistent security (uniform password policy) + * + * USAGE: + * npm run bulk-password -- + * # or with tsx: + * tsx src/scripts/bulk-password-protect.ts + * + * EXAMPLE: + * npm run bulk-password -- ../../test_files/test-pdfs ./output MySecureP@ss123 + */ + +import { program } from 'commander'; +import { writeFile } from 'fs/promises'; +import { join } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('bulk-password-protect') + .description('Apply password protection to all PDF files in a directory') + .argument('', 'Input folder containing PDF documents') + .argument('', 'Output folder for protected PDFs') + .argument('', 'Password for protection (min 6 characters)') + .action(async (inputFolder: string, outputFolder: string, password: string) => { + try { + // Validate password strength + if (password.length < 6) { + console.error('❌ Error: Password must be at least 6 characters long'); + process.exit(1); + } + + // Validate and setup (only process PDF files) + const files = await validateAndSetup(inputFolder, outputFolder, ['*.pdf']); + console.log(`📋 Found ${files.length} PDF document(s) to protect\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const pdfFile = files[i]; + const fileName = pdfFile.split('/').pop() || pdfFile; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Apply password protection + console.log(' 🔐 Applying password protection...'); + const protectedPdf = await client.passwordProtect(pdfFile, password); + + // Save protected PDF + const outputFile = join(outputFolder, fileName); + await writeFile(outputFile, protectedPdf); + + console.log(` ✅ Protected: ${fileName}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) password protected`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - remain unprotected!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log(`🔑 Password: ${'*'.repeat(password.length)} (${password.length} characters)`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Bulk password protection FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/convert-cli.ts b/samples/typescript/src/scripts/convert-cli.ts new file mode 100644 index 0000000..55b573f --- /dev/null +++ b/samples/typescript/src/scripts/convert-cli.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/** + * 🔄 SINGLE DOCUMENT CONVERSION + * ============================== + * + * The script exemplifies a typical workflow for quick document format conversion. + * As a business professional, you often need to convert individual + * documents between formats for sharing, presentations, or compatibility requirements. + * Whether converting a Word document to PDF for distribution, an Excel spreadsheet to + * CSV for data processing, or a presentation to images for web display, manual + * conversion through multiple applications is inefficient. + * + * This workflow provides instant document conversion. The script takes a single input + * file and converts it to the specified output format using professional-grade + * conversion algorithms. The result is a high-quality converted file that preserves + * formatting, structure, and content fidelity, ready for immediate use. + * + * CONVERSION FEATURES: + * ✓ Multiple format support (PDF, DOCX, XLSX, PNG, etc.) + * ✓ High-fidelity conversion (preserves formatting) + * + * USAGE: + * npm run convert -- + * # or with tsx: + * tsx src/scripts/convert-cli.ts + * + * EXAMPLES: + * npm run convert -- document.docx document.pdf pdf + * npm run convert -- presentation.pptx slide.pdf pdf + * npm run convert -- spreadsheet.xlsx data.pdf pdf + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; + +program + .name('convert-cli') + .description('Convert a document from one format to another using the Platform API') + .argument('', 'Input file to convert') + .argument('', 'Output file path') + .argument('', 'Target format for conversion (pdf, docx, xlsx, pptx, png)') + .action(async (inputFile: string, outputFile: string, format: string) => { + try { + // Validate format + const validFormats = Object.values(OutputFormat); + if (!validFormats.includes(format.toLowerCase() as OutputFormat)) { + console.error(`❌ Error: Invalid format '${format}'. Valid formats: ${validFormats.join(', ')}`); + process.exit(1); + } + + const toFormat = format.toLowerCase() as OutputFormat; + + // Validate input file exists + try { + await stat(inputFile); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputFile}`); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputFile), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Get input file size + const inputStat = await stat(inputFile); + const inputFileName = inputFile.split('/').pop() || inputFile; + + // Convert document + console.log(`🔄 Converting ${inputFileName} to ${toFormat.toUpperCase()}...`); + const converted = await client.convert(inputFile, toFormat); + + // Save converted file + await writeFile(outputFile, converted); + + // Display success message + console.log('✅ Conversion successful!'); + console.log(`📄 Input: ${inputFileName} (${inputStat.size.toLocaleString()} bytes)`); + console.log(`📄 Output: ${outputFile.split('/').pop()} (${converted.length.toLocaleString()} bytes)`); + console.log(`📂 Saved to: ${outputFile}`); + } catch (error: any) { + console.error(`❌ Conversion FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/employee-policy-onboarding.ts b/samples/typescript/src/scripts/employee-policy-onboarding.ts new file mode 100644 index 0000000..8cee794 --- /dev/null +++ b/samples/typescript/src/scripts/employee-policy-onboarding.ts @@ -0,0 +1,255 @@ +#!/usr/bin/env node +/** + * 📝 EMPLOYEE POLICY ONBOARDING + * ============================== + * + * This script exemplifies a typical HR onboarding workflow for new employees. + * As an HR professional, it's necessary to ensure all new hires review and sign + * required company policies before their start date. Manual distribution and + * tracking of signatures is time-consuming and error-prone, especially when + * onboarding multiple employees simultaneously. + * + * This workflow automates policy distribution and signature collection. The script + * processes each new employee individually - for every person in the CSV file, it + * creates a signature envelope containing all company policy documents, sends it + * via email with signature fields pre-configured, monitors the signing status, and + * automatically downloads the signed documents once completed. Each employee's + * signed policies are organized in their own folder, creating an audit-ready + * archive of onboarding documentation. + * + * NOTE: To run this script and see the complete workflow, you must provide a CSV file + * with valid employee names and email addresses. The script will send actual signature + * requests to these email addresses and wait for them to be signed. + * + * EMPLOYEE CSV FORMAT: + * name,email + * John Doe,john.doe@company.com + * Jane Smith,jane.smith@company.com + * Bob Johnson,bob.johnson@company.com + * + * USAGE: + * npm run employee-policy -- + * # or with tsx: + * tsx src/scripts/employee-policy-onboarding.ts + * + * EXAMPLE: + * npm run employee-policy -- ./policies ./new_hires_jan2025.csv + * + * OUTPUT STRUCTURE: + * output/ + * ├── john-doe/ + * │ ├── signed-documents/ + * │ ├── code-of-conduct-signed.pdf + * │ ├── confidentiality-agreement-signed.pdf + * │ └── audit-trail.pdf + * ├── jane-smith/ + * ├── signed-documents/ + */ + +import { program } from 'commander'; +import { stat, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { SignAPIClient } from '../api/sign-api.js'; +import { + loadEmployeesFromCsv, + loadPolicyDocumentsFromFolder, + createEmployeeFolderName, + createSignatureEnvelope, + addSignatureFieldsToDocuments, + sendAndMonitorEnvelope, + downloadSignedDocument, + logStep, +} from '../helpers/sign-helpers.js'; + +/** + * Custom error for envelope not signed in time. + */ +class EnvelopeNotSignedError extends Error { + constructor(message: string) { + super(message); + this.name = 'EnvelopeNotSignedError'; + } +} + +/** + * Validate inputs and load data. + * @param policiesFolder - Path to folder containing policy PDFs. + * @param employeesCsv - Path to CSV file with employee data. + * @returns Tuple of [outputFolder, employees, documents]. + */ +async function validateAndSetupInputs( + policiesFolder: string, + employeesCsv: string +): Promise<[string, Array<{ name: string; email: string }>, Array<{ name: string; binary: Buffer; path: string }>]> { + // Validate inputs + try { + const policiesStat = await stat(policiesFolder); + if (!policiesStat.isDirectory()) { + console.error(`❌ Policies folder not found: ${policiesFolder}`); + process.exit(1); + } + } catch (error) { + console.error(`❌ Policies folder not found: ${policiesFolder}`); + process.exit(1); + } + + try { + await stat(employeesCsv); + } catch (error) { + console.error(`❌ Employees CSV not found: ${employeesCsv}`); + process.exit(1); + } + + // Display header + const outputFolder = 'output'; + console.log('='.repeat(60)); + console.log('📝 SEND POLICIES TO EMPLOYEES'); + console.log('='.repeat(60)); + console.log(`Policies: ${policiesFolder}`); + console.log(`Employees: ${employeesCsv}`); + console.log(`Output: ${outputFolder}`); + console.log('='.repeat(60)); + console.log(); + + // Load employees and documents + const employees = await loadEmployeesFromCsv(employeesCsv); + console.log(`👥 Found ${employees.length} employee(s)\n`); + + const documents = await loadPolicyDocumentsFromFolder(policiesFolder); + + // Create output folder + await mkdir(outputFolder, { recursive: true }); + + return [outputFolder, employees, documents]; +} + +/** + * Process onboarding workflow for a single employee. + * @param signClient - Sign API client instance. + * @param employee - Employee data object with 'name' and 'email'. + * @param documents - Array of policy documents to send. + * @param outputFolder - Base output folder for signed documents. + * @param employeeNum - Current employee number (for display). + * @param totalEmployees - Total number of employees (for display). + */ +async function processEmployeeOnboarding( + signClient: SignAPIClient, + employee: { name: string; email: string }, + documents: Array<{ name: string; binary: Buffer; path: string }>, + outputFolder: string, + employeeNum: number, + totalEmployees: number +): Promise { + const { name, email } = employee; + + console.log(`\n[${employeeNum}/${totalEmployees}] ${name}`); + + // Create employee-specific output folder + const employeeFolder = join(outputFolder, createEmployeeFolderName(name)); + await mkdir(employeeFolder, { recursive: true }); + + // Create envelope and upload documents + logStep('📝 Creating envelope...'); + const [envelopeId, documentIds] = await createSignatureEnvelope( + signClient, + documents, + name, + email + ); + + // Add participant (signer) + logStep('👤 Adding signer...'); + const participantResponse = await signClient.createParticipant(envelopeId, { + email, + role: 'signer', + name, + }); + const participantId = participantResponse.ID; + + // Add signature fields to all documents + logStep('✍️ Adding fields...'); + await addSignatureFieldsToDocuments(signClient, envelopeId, documentIds, participantId); + + // Send and monitor envelope + logStep('📤 Sending...'); + const status = await sendAndMonitorEnvelope(signClient, envelopeId, email, 60); + + if (status !== 'sealed') { + throw new EnvelopeNotSignedError(`Envelope not signed: ${status}`); + } + + // Download signed documents + logStep('📥 Downloading...'); + await downloadSignedDocument(signClient, envelopeId, employeeFolder, 'signed-policies.zip'); + + console.log(' ✅ Completed\n'); +} + +program + .name('employee-policy-onboarding') + .description('Send company policy documents to employees for electronic signature via Sign API') + .argument('', 'Folder containing policy PDF documents') + .argument('', 'CSV file with employee data (name,email columns)') + .action(async (policiesFolder: string, employeesCsv: string) => { + try { + // Validate inputs and load data + const [outputFolder, employees, documents] = await validateAndSetupInputs( + policiesFolder, + employeesCsv + ); + + // Initialize Sign API client + const signClient = new SignAPIClient(); + + // Process each employee + console.log('='.repeat(60)); + console.log(`📤 PROCESSING ${employees.length} EMPLOYEE(S)`); + console.log('='.repeat(60)); + + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < employees.length; i++) { + const employee = employees[i]; + + try { + await processEmployeeOnboarding( + signClient, + employee, + documents, + outputFolder, + i + 1, + employees.length + ); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} employee(s) completed`); + if (failedCount > 0) { + console.log(`❌ ${failedCount} failed`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + if (error.message === 'SIGINT') { + console.log('\n\n⚠️ Interrupted by user'); + process.exit(1); + } + console.error(`\n❌ Error: ${error.message}`); + process.exit(1); + } + }); + +// Handle SIGINT (Ctrl+C) +process.on('SIGINT', () => { + console.log('\n\n⚠️ Interrupted by user'); + process.exit(1); +}); + +program.parse(); diff --git a/samples/typescript/src/scripts/extract-data.ts b/samples/typescript/src/scripts/extract-data.ts new file mode 100644 index 0000000..1e8d5c4 --- /dev/null +++ b/samples/typescript/src/scripts/extract-data.ts @@ -0,0 +1,118 @@ +#!/usr/bin/env node +/** + * 📊 DOCUMENT DATA EXTRACTION + * ============================ + * + * The script exemplifies a typical workflow for intelligent document data extraction. + * As a data analyst, you need to extract structured data from PDF documents - whether + * form fields from applications, surveys, and questionnaires, or table data from + * reports, invoices, and financial statements. Manual data entry is error-prone and + * time-consuming, especially when processing hundreds of documents for analysis or + * database import. + * + * This workflow automates data extraction using AI-powered document understanding. + * The script analyzes PDF documents and intelligently identifies and extracts either + * form fields (with field names and values) or table structures (with rows, columns, + * and cell contents). The extracted data is saved as structured JSON, ready for + * immediate integration with databases, spreadsheets, or analytics pipelines. + * + * DATA EXTRACTION FEATURES: + * ✓ AI-powered form field extraction + * ✓ Intelligent table detection and extraction + * ✓ Structured JSON output format + * ✓ High accuracy recognition + * + * USAGE: + * npm run extract -- + * # or with tsx: + * tsx src/scripts/extract-data.ts + * + * MODES: + * forms - Extract form fields (name-value pairs) + * tables - Extract table data (rows and columns) + * + * EXAMPLES: + * npm run extract -- forms application.pdf data.json + * npm run extract -- tables invoice.pdf tables.json + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; + +program + .name('extract-data') + .description('Extract structured data (forms or tables) from PDF documents') + .argument('', "Extraction mode: 'forms' or 'tables'") + .argument('', 'Input PDF file') + .argument('', 'Output JSON file') + .action(async (mode: string, inputPdf: string, outputJson: string) => { + try { + // Normalize mode to lowercase + mode = mode.toLowerCase(); + + // Validate mode + if (!['forms', 'tables'].includes(mode)) { + console.error("❌ Error: Mode must be 'forms' or 'tables'"); + process.exit(1); + } + + // Validate input file exists + try { + await stat(inputPdf); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputPdf}`); + process.exit(1); + } + + // Validate input is a PDF + if (!inputPdf.toLowerCase().endsWith('.pdf')) { + console.error('❌ Error: Input must be a PDF file'); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputJson), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Extract data based on mode + let data: any; + let dataType: string; + + if (mode === 'forms') { + console.log(`📋 Extracting form fields from ${inputPdf.split('/').pop()}...`); + data = await client.extractForms(inputPdf); + dataType = 'form fields'; + } else { + // mode === 'tables' + console.log(`📊 Extracting table data from ${inputPdf.split('/').pop()}...`); + data = await client.extractTables(inputPdf); + dataType = 'tables'; + } + + // Count extracted items + const result = data.result || {}; + const itemCount = + mode === 'forms' + ? (result.fields || []).length + : (result.tables || []).length; + + // Save extracted data as JSON + await writeFile(outputJson, JSON.stringify(data, null, 2), 'utf-8'); + + // Display success message + console.log('✅ Extraction successful!'); + console.log(`📊 Extracted: ${itemCount} ${dataType}`); + console.log(`📄 Input: ${inputPdf.split('/').pop()}`); + console.log(`📄 Output: ${outputJson.split('/').pop()}`); + console.log(`📂 Saved to: ${outputJson}`); + } catch (error: any) { + console.error(`❌ Extraction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts new file mode 100644 index 0000000..52f9a22 --- /dev/null +++ b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts @@ -0,0 +1,126 @@ +#!/usr/bin/env node +/** + * 🔒 PREPARE PDF FOR DISTRIBUTION + * ================================ + * + * The script exemplifies a typical workflow of marketing brochure distribution. + * As a marketing professional, it's necessary to share company brochures externally + * while ensuring they comply with corporate distribution standards. Word document + * properties can expose internal information such as author names, template paths, + * revision history, and company file structures that should remain confidential. + * + * This workflow automates compliant document preparation. The script processes each + * file individually - for every brochure in the input folder, it converts the Word + * document into PDF format, then compresses the file to reduce size and optimize + * transmission, and finally removes all metadata properties to ensure privacy and + * confidentiality. Each processed file is saved to the output folder, resulting in + * distribution-ready brochures. + * + * COMPANY DISTRIBUTION STANDARDS: + * ✓ PDF format (prevents editing) + * ✓ Compressed (optimized file size) + * ✓ Properties removed (no metadata exposure) + * ⏳ Annotations removed (feature in development) + * ⏳ Accessibility enabled (feature in development) + * + * USAGE: + * npm run prepare-pdf -- + * # or with tsx: + * tsx src/scripts/prepare-pdf-for-distribution.ts + * + * EXAMPLE: + * npm run prepare-pdf -- ../../test_files/test-batch ./output + */ + +import { program } from 'commander'; +import { writeFile, unlink } from 'fs/promises'; +import { join, parse } from 'path'; +import { PlatformAPIClient, OutputFormat } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +// Configuration: Properties to remove from PDFs +const PROPERTIES_TO_REMOVE = ['title', 'author', 'subject', 'keywords', 'creator', 'producer']; + +program + .name('prepare-pdf-for-distribution') + .description('Prepare documents for distribution by converting to PDF and removing metadata') + .argument('', 'Input folder containing documents') + .argument('', 'Output folder for prepared PDFs') + .action(async (inputFolder: string, outputFolder: string) => { + try { + // Validate and setup + const files = await validateAndSetup(inputFolder, outputFolder); + console.log(`📋 Found ${files.length} document(s) to process\n`); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + + for (let i = 0; i < files.length; i++) { + const doc = files[i]; + const docName = doc.split('/').pop() || doc; + const docBaseName = parse(doc).name; + + console.log(`[${i + 1}/${files.length}] Processing: ${docName}`); + + let tempPdf: string | null = null; + + try { + // Step 1: Convert to PDF + console.log(' 🔐 Converting to PDF...'); + const pdfBytes = await client.convert(doc, OutputFormat.PDF); + + tempPdf = join(outputFolder, `${docBaseName}_temp.pdf`); + await writeFile(tempPdf, pdfBytes); + + // Step 2: Compress PDF + console.log(' 📦 Compressing...'); + const compressedPdf = await client.compress(tempPdf, 2); + await writeFile(tempPdf, compressedPdf); + + // Step 3: Remove metadata properties + console.log(' 🔒 Removing metadata...'); + const propertiesToClear: Record = {}; + for (const prop of PROPERTIES_TO_REMOVE) { + propertiesToClear[prop] = ''; + } + const cleanPdf = await client.setProperties(tempPdf, propertiesToClear); + + // Save final PDF + const finalPdf = join(outputFolder, `${docBaseName}.pdf`); + await writeFile(finalPdf, cleanPdf); + await unlink(tempPdf); + + console.log(` ✅ Secured: ${parse(finalPdf).base}\n`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}\n`); + failedCount++; + if (tempPdf) { + try { + await unlink(tempPdf); + } catch (e) { + // Ignore cleanup errors + } + } + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) secured`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - do NOT distribute!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ PDF preparation FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/quickstart.ts b/samples/typescript/src/scripts/quickstart.ts new file mode 100644 index 0000000..bb112d1 --- /dev/null +++ b/samples/typescript/src/scripts/quickstart.ts @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Quickstart script to test Nitro Platform API authentication and connection. + */ + +import dotenv from 'dotenv'; +import axios from 'axios'; + +dotenv.config(); + +const BASE_URL = process.env.PLATFORM_BASE_URL || 'https://api.gonitro.dev'; +const CLIENT_ID = process.env.PLATFORM_CLIENT_ID; +const CLIENT_SECRET = process.env.PLATFORM_CLIENT_SECRET; + +/** + * Get OAuth2 access token using client credentials. + * @returns Access token string. + */ +async function getAccessToken(): Promise { + const url = `${BASE_URL}/oauth/token`; + const data = { + clientID: CLIENT_ID, + clientSecret: CLIENT_SECRET, + }; + + const response = await axios.post(url, data); + return response.data.accessToken; +} + +/** + * Test API connection with a simple request. + * @param token - OAuth2 access token. + * @returns True if connection is successful. + */ +async function testConnection(token: string): Promise { + const url = `${BASE_URL}/jobs/test-job-id/status`; + const headers = { Authorization: `Bearer ${token}` }; + + try { + await axios.get(url, { headers }); + return true; + } catch (error: any) { + // 404 is expected for non-existent job, but proves auth works + if (error.response?.status === 404) { + console.log('✅ Authentication successful (404 expected for test job ID)'); + return true; + } + throw error; + } +} + +/** + * Main function to test API authentication and connection. + */ +async function main(): Promise { + if (!CLIENT_ID || !CLIENT_SECRET) { + console.log('❌ Missing credentials!'); + console.log('To get your credentials:'); + console.log('1. Go to https://admin.gonitro.com'); + console.log('2. Navigate to Settings → API'); + console.log("3. Click 'Create Application'"); + console.log('4. Name your application and save the Client ID and Client Secret'); + console.log('5. Set environment variables:'); + console.log(' export PLATFORM_CLIENT_ID='); + console.log(' export PLATFORM_CLIENT_SECRET='); + return; + } + + try { + console.log('🔐 Getting access token...'); + const token = await getAccessToken(); + console.log('✅ Token obtained successfully'); + + console.log('🧪 Testing API connection...'); + await testConnection(token); + console.log('✅ API connection successful'); + + console.log('\n🎉 Setup complete! You can now use the Platform API.'); + console.log('📖 See https://developers.gonitro.com/docs for API documentation'); + } catch (error: any) { + console.error(`❌ Error: ${error.message}`); + process.exit(1); + } +} + +main(); diff --git a/samples/typescript/src/scripts/redact-by-keyword.ts b/samples/typescript/src/scripts/redact-by-keyword.ts new file mode 100644 index 0000000..a7c1728 --- /dev/null +++ b/samples/typescript/src/scripts/redact-by-keyword.ts @@ -0,0 +1,116 @@ +#!/usr/bin/env node +/** + * 🔍 KEYWORD-BASED REDACTION + * =========================== + * + * The script exemplifies a typical workflow for targeted content redaction. + * As a compliance officer, you need to redact specific sensitive terms from documents + * before external sharing or public disclosure. Whether removing client names, project + * codenames, financial figures, or proprietary terminology, manually searching through + * pages and applying redactions is tedious and risks missing instances, potentially + * exposing confidential information. + * + * This workflow automates keyword-based redaction. The script searches the entire + * PDF document for all specified keywords and phrases, identifies their exact + * locations across all pages, then automatically applies permanent redactions to + * remove them. Multiple keywords can be processed in a single pass, ensuring + * comprehensive coverage. The result is a thoroughly redacted document ready for + * safe distribution. + * + * KEYWORD REDACTION FEATURES: + * ✓ Multi-keyword search (process multiple terms) + * ✓ Whole document scanning (all pages) + * ✓ Exact location detection + * ✓ Permanent redaction (unrecoverable) + * ⏳ Case-insensitive matching (feature in development) + * ⏳ Regex pattern support (feature in development) + * + * USAGE: + * npm run redact-keyword -- [keyword2 ...] + * # or with tsx: + * tsx src/scripts/redact-by-keyword.ts [keyword2 ...] + * + * EXAMPLES: + * npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" + * npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" + */ + +import { program } from 'commander'; +import { stat, mkdir, writeFile, copyFile } from 'fs/promises'; +import { dirname } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; + +program + .name('redact-by-keyword') + .description('Redact specific keywords from PDF documents using text search') + .argument('', 'Input PDF file to redact') + .argument('', 'Output PDF file with redactions') + .argument('', 'Keywords to search for and redact') + .action(async (inputPdf: string, outputPdf: string, keywords: string[]) => { + try { + // Validate input file exists + try { + await stat(inputPdf); + } catch (error) { + console.error(`❌ Error: Input file not found: ${inputPdf}`); + process.exit(1); + } + + // Validate input is a PDF + if (!inputPdf.toLowerCase().endsWith('.pdf')) { + console.error('❌ Error: Input must be a PDF file'); + process.exit(1); + } + + // Create output directory if needed + await mkdir(dirname(outputPdf), { recursive: true }); + + // Initialize API client (loads credentials from .env) + const client = new PlatformAPIClient(); + + // Step 1: Search for keywords in document + console.log(`🔍 Searching for ${keywords.length} keyword(s) in ${inputPdf.split('/').pop()}...`); + console.log(` Keywords: ${keywords.map(k => `"${k}"`).join(', ')}`); + + const bboxData = await client.findTextBoxes(inputPdf, keywords); + + // Extract text box locations from response + const textBoxes = bboxData.result?.textBoxes || []; + + if (textBoxes.length === 0) { + console.log('ℹ️ No keyword matches found - copying original file'); + // Copy original file to output if no keywords found + await copyFile(inputPdf, outputPdf); + console.log(`✅ Saved: ${outputPdf.split('/').pop()}`); + console.log(`📂 Output: ${outputPdf}`); + return; + } + + console.log(`🎯 Found ${textBoxes.length} keyword instance(s) to redact`); + + // Step 2: Prepare redaction coordinates + console.log('🔒 Applying redactions...'); + const redactions = textBoxes.map((box: any) => ({ + pageIndex: box.pageIndex, + boundingBox: box.boundingBox, + })); + + // Step 3: Apply redactions to document + const redactedPdf = await client.redact(inputPdf, redactions); + + // Save redacted PDF + await writeFile(outputPdf, redactedPdf); + + // Display success message + console.log('✅ Redaction successful!'); + console.log(`🔒 Redacted: ${textBoxes.length} instance(s)`); + console.log(`📄 Input: ${inputPdf.split('/').pop()}`); + console.log(`📄 Output: ${outputPdf.split('/').pop()}`); + console.log(`📂 Saved to: ${outputPdf}`); + } catch (error: any) { + console.error(`❌ Redaction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/src/scripts/smart-redact-pii.ts b/samples/typescript/src/scripts/smart-redact-pii.ts new file mode 100644 index 0000000..7ab5da1 --- /dev/null +++ b/samples/typescript/src/scripts/smart-redact-pii.ts @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * 🔒 SMART PII REDACTION + * ====================== + * + * The script exemplifies a typical workflow for protecting sensitive customer information. + * As a compliance officer, it's essential to review and redact personally identifiable + * information (PII) from documents before sharing them with third parties, storing them + * in public systems, or using them for analysis. Manual redaction is time-consuming and + * error-prone, potentially missing sensitive data like social security numbers, phone + * numbers, addresses, or email addresses. + * + * This workflow automates compliant document redaction. The script processes each PDF + * file individually - for every document in the input folder, it uses AI-powered PII + * detection to identify all instances of sensitive information across all pages, then + * automatically applies redactions to permanently remove this data. Each processed file + * is saved to the output folder with all PII securely redacted, ready for safe sharing + * or archival. + * + * PRIVACY COMPLIANCE STANDARDS: + * ✓ AI-powered PII detection (SSN, phone, email, address) + * ✓ Automatic redaction (permanent removal) + * ✓ Batch processing (entire folders) + * + * USAGE: + * npm run smart-redact -- + * # or with tsx: + * tsx src/scripts/smart-redact-pii.ts + * + * EXAMPLE: + * npm run smart-redact -- ../../test_files/test-pdfs ./output + */ + +import { program } from 'commander'; +import { readFile, writeFile, copyFile } from 'fs/promises'; +import { join } from 'path'; +import { PlatformAPIClient } from '../api/platform-api.js'; +import { validateAndSetup } from '../helpers/document-helpers.js'; + +program + .name('smart-redact-pii') + .description('Automatically detect and redact PII (personally identifiable information) from PDFs') + .argument('', 'Input folder containing PDF documents') + .argument('', 'Output folder for redacted PDFs') + .action(async (inputFolder: string, outputFolder: string) => { + try { + // Validate and setup (only process PDF files) + const files = await validateAndSetup(inputFolder, outputFolder, ['*.pdf']); + console.log(`📋 Found ${files.length} PDF document(s) to process`); + + // Initialize API client + const client = new PlatformAPIClient(); + + // Process each document + let successCount = 0; + let failedCount = 0; + let totalPiiCount = 0; + + for (let i = 0; i < files.length; i++) { + const pdfFile = files[i]; + const fileName = pdfFile.split('/').pop() || pdfFile; + + console.log(`[${i + 1}/${files.length}] Processing: ${fileName}`); + + try { + // Step 1: Detect PII in the document + console.log(' 🔍 Detecting PII...'); + const piiData = await client.detectPii(pdfFile); + + // Extract PII bounding boxes from response + const piiBoxes = piiData.result?.PIIBoxes || []; + + if (piiBoxes.length === 0) { + console.log(' ℹ️ No PII detected - copying original file'); + + // Copy original file to output if no PII found + const outputFile = join(outputFolder, fileName); + await copyFile(pdfFile, outputFile); + + console.log(` ✅ Saved: ${fileName}`); + successCount++; + continue; + } + + console.log(` 🎯 Found ${piiBoxes.length} PII instance(s)`); + totalPiiCount += piiBoxes.length; + + // Step 2: Prepare redaction coordinates + console.log(' 🔒 Applying redactions...'); + const redactions = piiBoxes.map((box: any) => ({ + pageIndex: box.pageIndex, + boundingBox: box.boundingBox, + })); + + // Step 3: Apply redactions to document + const redactedPdf = await client.redact(pdfFile, redactions); + + // Save redacted PDF + const outputFile = join(outputFolder, fileName); + await writeFile(outputFile, redactedPdf); + + console.log(` ✅ Redacted: ${fileName}`); + successCount++; + } catch (error: any) { + console.log(` ❌ FAILED: ${error.message}`); + failedCount++; + } + } + + // Display summary + console.log('='.repeat(60)); + console.log(`✅ ${successCount} document(s) processed`); + console.log(`🔒 ${totalPiiCount} total PII instance(s) redacted`); + if (failedCount > 0) { + console.log(`⚠️ ${failedCount} document(s) FAILED - review manually!`); + } + console.log(`📂 Output: ${outputFolder}`); + console.log('='.repeat(60)); + } catch (error: any) { + console.error(`❌ Smart redaction FAILED: ${error.message}`); + process.exit(1); + } + }); + +program.parse(); diff --git a/samples/typescript/tsconfig.json b/samples/typescript/tsconfig.json new file mode 100644 index 0000000..72093ae --- /dev/null +++ b/samples/typescript/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 4547d6b5a0c69a07c1b761fa6f2e936f6ef7bd6b Mon Sep 17 00:00:00 2001 From: isadoraPGoNitro Date: Tue, 6 Jan 2026 20:45:26 +0000 Subject: [PATCH 19/19] updated typescript scripts --- samples/python/README.md | 155 ++- samples/typescript/README.md | 105 ++ samples/typescript/package-lock.json | 936 ++++++++++++++++++ .../typescript/src/scripts/batch-process.ts | 56 +- .../src/scripts/bulk-password-protect.ts | 56 +- samples/typescript/src/scripts/convert-cli.ts | 54 +- .../src/scripts/employee-policy-onboarding.ts | 87 +- .../typescript/src/scripts/extract-data.ts | 67 +- .../scripts/prepare-pdf-for-distribution.ts | 57 +- .../src/scripts/redact-by-keyword.ts | 64 +- .../src/scripts/smart-redact-pii.ts | 60 +- 11 files changed, 1438 insertions(+), 259 deletions(-) create mode 100644 samples/typescript/package-lock.json diff --git a/samples/python/README.md b/samples/python/README.md index d37a260..e3ddfeb 100644 --- a/samples/python/README.md +++ b/samples/python/README.md @@ -316,10 +316,163 @@ python convert_cli.py ../../test_files/test-batch/Analysis.docx /tmp/output.pdf ./test_all.sh ``` -## Sample Files +## Testing with Sample Files + +The repository includes test files you can use to try out the scripts. All test files are located in the `../../test_files/` directory (relative to the Python samples folder). + +### Quick Test Commands + +#### 1. Test Authentication +```bash +uv run python quickstart.py +# Expected: ✅ Authentication successful! Token: eyJ0... +``` + +#### 2. Convert Single Document +```bash +# Convert Word document to PDF +uv run python convert_cli.py ../../test_files/test-batch/Analysis.docx output/test_output.pdf pdf + +# Convert Excel to PDF +uv run python convert_cli.py ../../test_files/test-batch/Feedback.xlsx output/test_output.pdf pdf + +# Convert PowerPoint to PNG +uv run python convert_cli.py ../../test_files/SamplePPTX.pptx output/test_output.png png +``` + +#### 3. Batch Document Conversion +```bash +# Convert all Word documents in test-batch to PDF +uv run python batch_process.py \ + ../../test_files/test-batch \ + output/batch_results \ + pdf \ + "*.docx" + +# Convert all Excel files +uv run python batch_process.py \ + ../../test_files/test-batch \ + output/batch_results \ + pdf \ + "*.xlsx" +``` + +#### 4. Extract Data from PDFs +```bash +# Extract form fields from student loan application +uv run python extract_data.py \ + forms \ + "../../test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf" \ + output/forms_output.json + +# Extract tables from PDF +uv run python extract_data.py \ + tables \ + "../../test_files/test-pdfs/Sample Tables.pdf" \ + output/tables_output.json +``` + +#### 5. Smart PII Redaction +```bash +# Automatically detect and redact PII from PDFs +uv run python smart_redact_pii.py \ + ../../test_files/test-pdfs \ + output/pii_redacted +``` + +#### 6. Keyword-Based Redaction +```bash +# Redact specific keywords from resume +uv run python redact_by_keyword.py \ + ../../test_files/test-pdfs/SampleResume.pdf \ + output/redacted.pdf \ + "resume" "contact" +``` + +#### 7. Bulk Password Protection +```bash +# Password protect all PDFs in test-pdfs folder +uv run python bulk_password_protect.py \ + ../../test_files/test-pdfs \ + output/protected \ + "SecurePass123" +``` + +#### 8. Prepare PDFs for Distribution +```bash +# Convert marketing brochures to optimized PDFs with metadata removed +uv run python prepare_pdf_for_distribution.py \ + ../../test_files/pdf-distribution \ + output/distribution +``` + +#### 9. Employee Policy Onboarding (Sign API) +```bash +# Send company policies for signature to employees +uv run python employee_policy_onboarding.py \ + ../../test_files/test-sign \ + ../../test_files/test-sign/employees.csv + +# Note: This sends real signature requests to email addresses in the CSV! +# Check the CSV file first: ../../test_files/test-sign/employees.csv +``` + +### Using Task Commands + +All the above can also be run using Task commands for convenience: + +```bash +# Convert +task convert INPUT=../../test_files/test-batch/Analysis.docx OUTPUT=output/test.pdf FORMAT=pdf + +# Batch process +task batch INPUT_DIR=../../test_files/test-batch OUTPUT_DIR=output/batch FORMAT=pdf PATTERN='*.docx' + +# Extract data +task extract MODE=tables INPUT="../../test_files/test-pdfs/Sample Tables.pdf" OUTPUT=output/tables.json + +# Smart redact +task smart-redact INPUT_DIR=../../test_files/test-pdfs OUTPUT_DIR=output/redacted + +# Prepare PDFs +task prepare-distribution INPUT_DIR=../../test_files/pdf-distribution OUTPUT_DIR=output/distribution + +# Employee onboarding +task onboard-employees POLICIES_DIR=../../test_files/test-sign CSV=../../test_files/test-sign/employees.csv +``` + +### Available Test Files Sample files for testing are available in the `test_files/` folder at the repository root. +#### Batch Conversion (`test_files/test-batch/`) +- `Analysis.docx` - Word document +- `Feedback.xlsx` - Excel spreadsheet + +#### PDF Operations (`test_files/test-pdfs/`) +- `SampleResume.pdf` - Resume with PII (for redaction testing) +- `Sample Tables.pdf` - PDF with tables (for extraction) +- `BOB - Student-Loan-Application-Form.pdf` - Form with fields (for extraction) + +#### Distribution (`test_files/pdf-distribution/`) +- `Marketing_Brochure_Product_A_Rich.docx` +- `Marketing_Brochure_Product_B_Rich.docx` +- `Marketing_Brochure_Product_C_Rich.docx` + +#### Sign API (`test_files/test-sign/`) +- `company-policies.pdf` - Company policy document +- `confidentiality-agreement.pdf` - NDA template +- `sample-company-policies.pdf` - Additional policy +- `employees.csv` - Sample employee list for testing + +### Important Notes + +- **Sign API Testing**: The `employee_policy_onboarding` script sends real signature requests via email. Make sure the email addresses in `employees.csv` are valid and you have permission to send them test requests. +- **Output Folders**: All scripts automatically create output folders if they don't exist. +- **File Paths**: Use quotes around file paths with spaces (e.g., `"Sample Tables.pdf"`). +- **Large Files**: Some operations may take longer with large or complex documents. + + ## Getting Your Credentials 1. Go to [https://admin.gonitro.com](https://admin.gonitro.com) diff --git a/samples/typescript/README.md b/samples/typescript/README.md index ac311b8..008fa9c 100644 --- a/samples/typescript/README.md +++ b/samples/typescript/README.md @@ -258,6 +258,111 @@ output/ └── envelope-info.json ``` +## Testing with Sample Files + +The repository includes test files you can use to try out the scripts. All test files are located in the `../../test_files/` directory (relative to the TypeScript samples folder). + +### Quick Test Commands + +#### 1. Test Authentication +```bash +npm run quickstart +# Expected: ✅ Authentication successful! Token: eyJ0... +``` + +#### 2. Convert Single Document +```bash +# Convert Word document to PDF +npm run convert -- ../../test_files/test-batch/Analysis.docx output.pdf pdf + +# Convert Excel to PDF +npm run convert -- ../../test_files/test-batch/Feedback.xlsx output.pdf pdf + +# Convert PowerPoint to PNG +npm run convert -- ../../test_files/SamplePPTX.pptx output.png png +``` + +#### 3. Batch Document Conversion +```bash +# Convert all Word documents in test-batch to PDF +npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" + +# Convert all Excel files +npm run batch -- ../../test_files/test-batch ./output pdf "*.xlsx" +``` + +#### 4. Extract Data from PDFs +```bash +# Extract form fields from student loan application +npm run extract -- forms "../../test_files/test-pdfs/BOB - Student-Loan-Application-Form.pdf" forms-output.json + +# Extract tables from PDF +npm run extract -- tables "../../test_files/test-pdfs/Sample Tables.pdf" tables-output.json +``` + +#### 5. Smart PII Redaction +```bash +# Automatically detect and redact PII from resume +npm run smart-redact -- ../../test_files/test-pdfs ./redacted-output +``` + +#### 6. Keyword-Based Redaction +```bash +# Redact specific keywords from resume +npm run redact-keyword -- ../../test_files/test-pdfs/SampleResume.pdf redacted.pdf "resume" "contact" +``` + +#### 7. Bulk Password Protection +```bash +# Password protect all PDFs in test-pdfs folder +npm run bulk-password -- ../../test_files/test-pdfs ./protected-output "SecurePass123" +``` + +#### 8. Prepare PDFs for Distribution +```bash +# Convert marketing brochures to optimized PDFs with metadata removed +npm run prepare-pdf -- ../../test_files/pdf-distribution ./distribution-output +``` + +#### 9. Employee Policy Onboarding (Sign API) +```bash +# Send company policies for signature to employees +npm run employee-policy -- ../../test_files/test-sign ../../test_files/test-sign/employees.csv + +# Note: This sends real signature requests to email addresses in the CSV! +# Check the CSV file first: ../../test_files/test-sign/employees.csv +``` + +### Available Test Files + +#### Batch Conversion (`test_files/test-batch/`) +- `Analysis.docx` - Word document +- `Feedback.xlsx` - Excel spreadsheet + +#### PDF Operations (`test_files/test-pdfs/`) +- `SampleResume.pdf` - Resume with PII (for redaction testing) +- `Sample Tables.pdf` - PDF with tables (for extraction) +- `BOB - Student-Loan-Application-Form.pdf` - Form with fields (for extraction) + +#### Distribution (`test_files/pdf-distribution/`) +- `Marketing_Brochure_Product_A_Rich.docx` +- `Marketing_Brochure_Product_B_Rich.docx` +- `Marketing_Brochure_Product_C_Rich.docx` + +#### Sign API (`test_files/test-sign/`) +- `company-policies.pdf` - Company policy document +- `confidentiality-agreement.pdf` - NDA template +- `sample-company-policies.pdf` - Additional policy +- `employees.csv` - Sample employee list for testing + + +### Important Notes + +- **Sign API Testing**: The `employee-policy-onboarding` script sends real signature requests via email. Make sure the email addresses in `employees.csv` are valid and you have permission to send them test requests. +- **Output Folders**: All scripts automatically create output folders if they don't exist. +- **File Paths**: Use quotes around file paths with spaces (e.g., `"Sample Tables.pdf"`). +- **Large Files**: Some operations may take longer with large or complex documents. + ## Code Style & Conventions ### Naming Conventions diff --git a/samples/typescript/package-lock.json b/samples/typescript/package-lock.json new file mode 100644 index 0000000..8d0b96f --- /dev/null +++ b/samples/typescript/package-lock.json @@ -0,0 +1,936 @@ +{ + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@nitro/platform-samples-typescript", + "version": "0.1.0", + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.0", + "commander": "^11.0.0", + "csv-parse": "^5.5.0", + "dotenv": "^16.0.0", + "form-data": "^4.0.0", + "mime-types": "^2.1.35" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.5", + "@types/mime-types": "^2.1.4", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/csv-parse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/samples/typescript/src/scripts/batch-process.ts b/samples/typescript/src/scripts/batch-process.ts index 477bf08..49351fe 100644 --- a/samples/typescript/src/scripts/batch-process.ts +++ b/samples/typescript/src/scripts/batch-process.ts @@ -1,34 +1,32 @@ #!/usr/bin/env node /** - * 📁 BATCH DOCUMENT CONVERSION - * ============================= - * - * The script exemplifies a typical workflow for document format standardization. - * As an IT administrator, it's essential to convert large collections of documents - * into standardized formats for archival, compliance, or system integration purposes. - * Manually converting individual files through desktop applications is time-consuming - * and impractical for large document sets, often leading to inconsistent results and - * wasted effort. - * - * This workflow automates bulk document conversion. The script processes files - * individually - for every document matching the specified pattern in the input folder, - * it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using - * high-fidelity conversion algorithms. Each converted file is saved to the output - * folder with the same base name but the new format extension, resulting in a - * complete batch of standardized documents. - * - * BATCH CONVERSION FEATURES: - * ✓ Multiple format support (PDF, DOCX, XLSX, PNG, JPG, etc.) - * ✓ Flexible file pattern matching (*.docx, *.xlsx, *.pptx, *.pdf.) - * - * USAGE: - * npm run batch -- [pattern] - * # or with tsx: - * tsx src/scripts/batch-process.ts [pattern] - * - * EXAMPLES: - * npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" - * npm run batch -- ./documents ./converted png "*" + 📁 BATCH DOCUMENT CONVERSION +============================= + +The script exemplifies a typical workflow for document format standardization. +As a IT administrator, it's essential to convert +large collections of documents into standardized formats for archival, compliance, +or system integration purposes. Manually converting individual files through desktop +applications is time-consuming and impractical for large document sets, often leading +to inconsistent results and wasted effort. + +This workflow automates bulk document conversion. The script processes files +individually - for every document matching the specified pattern in the input folder, +it converts the file to the target format (PDF, DOCX, XLSX, PNG, JPG, etc.) using +high-fidelity conversion algorithms. Each converted file is saved to the output +folder with the same base name but the new format extension, resulting in a +complete batch of standardized documents. + +BATCH CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, JPG, etc.) + ✓ Flexible file pattern matching (*.docx, *.xlsx, *.pptx, *.pdf.) + +USAGE: + npm run batch -- [pattern] + +EXAMPLES: + npm run batch -- ../../test_files/test-batch ./output pdf "*.docx" + npm run batch -- ./documents ./converted png "*" */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/bulk-password-protect.ts b/samples/typescript/src/scripts/bulk-password-protect.ts index 0e05ed4..a4f468a 100644 --- a/samples/typescript/src/scripts/bulk-password-protect.ts +++ b/samples/typescript/src/scripts/bulk-password-protect.ts @@ -1,34 +1,32 @@ #!/usr/bin/env node /** - * 🔐 BULK PASSWORD PROTECTION - * ============================ - * - * The script exemplifies a typical workflow for securing confidential documents. - * As a security professional, it's essential to protect sensitive documents with - * passwords before distributing them to authorized personnel, storing them in shared - * drives, or archiving them for compliance purposes. Manually setting passwords on - * individual files is tedious and inconsistent, leading to weak passwords or missed - * files that remain unprotected. - * - * This workflow automates secure document protection. The script processes each PDF - * file individually - for every document in the input folder, it applies robust - * password encryption using a consistent password across all files. Each protected - * file is saved to the output folder with the same filename, ensuring that the entire - * batch of documents maintains uniform security standards. The result is a complete - * set of password-protected PDFs ready for secure distribution or storage. - * - * DOCUMENT SECURITY STANDARDS: - * ✓ Password encryption (AES-256) - * ✓ Batch processing (entire folders) - * ✓ Consistent security (uniform password policy) - * - * USAGE: - * npm run bulk-password -- - * # or with tsx: - * tsx src/scripts/bulk-password-protect.ts - * - * EXAMPLE: - * npm run bulk-password -- ../../test_files/test-pdfs ./output MySecureP@ss123 + 🔐 BULK PASSWORD PROTECTION +============================ + +The script exemplifies a typical workflow for securing confidential documents. +As a security professional, it's essential to protect sensitive +documents with passwords before distributing them to authorized personnel, storing +them in shared drives, or archiving them for compliance purposes. Manually setting +passwords on individual files is tedious and inconsistent, leading to weak passwords +or missed files that remain unprotected. + +This workflow automates secure document protection. The script processes each PDF +file individually - for every document in the input folder, it applies robust +password encryption using a consistent password across all files. Each protected +file is saved to the output folder with the same filename, ensuring that the entire +batch of documents maintains uniform security standards. The result is a complete +set of password-protected PDFs ready for secure distribution or storage. + +DOCUMENT SECURITY STANDARDS: + ✓ Password encryption (AES-256) + ✓ Batch processing (entire folders) + ✓ Consistent security (uniform password policy) + +USAGE: + npm run bulk-password -- + +EXAMPLE: + npm run bulk-password -- ../../test_files/test-pdfs ./output MySecureP@ss123 */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/convert-cli.ts b/samples/typescript/src/scripts/convert-cli.ts index 55b573f..7234cc2 100644 --- a/samples/typescript/src/scripts/convert-cli.ts +++ b/samples/typescript/src/scripts/convert-cli.ts @@ -1,33 +1,31 @@ #!/usr/bin/env node /** - * 🔄 SINGLE DOCUMENT CONVERSION - * ============================== - * - * The script exemplifies a typical workflow for quick document format conversion. - * As a business professional, you often need to convert individual - * documents between formats for sharing, presentations, or compatibility requirements. - * Whether converting a Word document to PDF for distribution, an Excel spreadsheet to - * CSV for data processing, or a presentation to images for web display, manual - * conversion through multiple applications is inefficient. - * - * This workflow provides instant document conversion. The script takes a single input - * file and converts it to the specified output format using professional-grade - * conversion algorithms. The result is a high-quality converted file that preserves - * formatting, structure, and content fidelity, ready for immediate use. - * - * CONVERSION FEATURES: - * ✓ Multiple format support (PDF, DOCX, XLSX, PNG, etc.) - * ✓ High-fidelity conversion (preserves formatting) - * - * USAGE: - * npm run convert -- - * # or with tsx: - * tsx src/scripts/convert-cli.ts - * - * EXAMPLES: - * npm run convert -- document.docx document.pdf pdf - * npm run convert -- presentation.pptx slide.pdf pdf - * npm run convert -- spreadsheet.xlsx data.pdf pdf +🔄 SINGLE DOCUMENT CONVERSION +============================== + +The script exemplifies a typical workflow for quick document format conversion. +As a business professional, you often need to convert individual +documents between formats for sharing, presentations, or compatibility requirements. +Whether converting a Word document to PDF for distribution, an Excel spreadsheet to +CSV for data processing, or a presentation to images for web display, manual +conversion through multiple applications is inefficient. + +This workflow provides instant document conversion. The script takes a single input +file and converts it to the specified output format using professional-grade +conversion algorithms. The result is a high-quality converted file that preserves +formatting, structure, and content fidelity, ready for immediate use. + +CONVERSION FEATURES: + ✓ Multiple format support (PDF, DOCX, XLSX, PNG, etc.) + ✓ High-fidelity conversion (preserves formatting) + +USAGE: + npm run convert -- + +EXAMPLES: + npm run convert -- document.docx document.pdf pdf + npm run convert -- presentation.pptx slide.pdf pdf + npm run convert -- spreadsheet.xlsx data.pdf pdf */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/employee-policy-onboarding.ts b/samples/typescript/src/scripts/employee-policy-onboarding.ts index 8cee794..fb598e0 100644 --- a/samples/typescript/src/scripts/employee-policy-onboarding.ts +++ b/samples/typescript/src/scripts/employee-policy-onboarding.ts @@ -1,49 +1,48 @@ #!/usr/bin/env node /** - * 📝 EMPLOYEE POLICY ONBOARDING - * ============================== - * - * This script exemplifies a typical HR onboarding workflow for new employees. - * As an HR professional, it's necessary to ensure all new hires review and sign - * required company policies before their start date. Manual distribution and - * tracking of signatures is time-consuming and error-prone, especially when - * onboarding multiple employees simultaneously. - * - * This workflow automates policy distribution and signature collection. The script - * processes each new employee individually - for every person in the CSV file, it - * creates a signature envelope containing all company policy documents, sends it - * via email with signature fields pre-configured, monitors the signing status, and - * automatically downloads the signed documents once completed. Each employee's - * signed policies are organized in their own folder, creating an audit-ready - * archive of onboarding documentation. - * - * NOTE: To run this script and see the complete workflow, you must provide a CSV file - * with valid employee names and email addresses. The script will send actual signature - * requests to these email addresses and wait for them to be signed. - * - * EMPLOYEE CSV FORMAT: - * name,email - * John Doe,john.doe@company.com - * Jane Smith,jane.smith@company.com - * Bob Johnson,bob.johnson@company.com - * - * USAGE: - * npm run employee-policy -- - * # or with tsx: - * tsx src/scripts/employee-policy-onboarding.ts - * - * EXAMPLE: - * npm run employee-policy -- ./policies ./new_hires_jan2025.csv - * - * OUTPUT STRUCTURE: - * output/ - * ├── john-doe/ - * │ ├── signed-documents/ - * │ ├── code-of-conduct-signed.pdf - * │ ├── confidentiality-agreement-signed.pdf - * │ └── audit-trail.pdf - * ├── jane-smith/ - * ├── signed-documents/ +📝 EMPLOYEE POLICY ONBOARDING +============================== + +This script exemplifies a typical HR onboarding workflow for new employees. +As an HR professional, it's necessary to ensure all new hires review and sign +required company policies before their start date. Manual distribution and +tracking of signatures is time-consuming and error-prone, especially when +onboarding multiple employees simultaneously. + +This workflow automates policy distribution and signature collection. The script +processes each new employee individually - for every person in the CSV file, it +creates a signature envelope containing all company policy documents, sends it +via email with signature fields pre-configured, monitors the signing status, and +automatically downloads the signed documents once completed. Each employee's +signed policies are organized in their own folder, creating an audit-ready +archive of onboarding documentation. + + +NOTE: To run this script and see the complete workflow, you must provide a CSV file +with valid employee names and email addresses. The script will send actual signature +requests to these email addresses and wait for them to be signed. + +EMPLOYEE CSV FORMAT: + name,email + John Doe,john.doe@company.com + Jane Smith,jane.smith@company.com + Bob Johnson,bob.johnson@company.com + +USAGE: + npm run employee-policy -- + + EXAMPLE: + npm run employee-policy -- ./policies ./new_hires_jan2025.csv + + OUTPUT STRUCTURE: + output/ + ├── john-doe/ + │ ├── signed-documents/ + │ ├── code-of-conduct-signed.pdf + │ ├── confidentiality-agreement-signed.pdf + │ └── audit-trail.pdf + ├── jane-smith/ + │ ├── signed-documents/ */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/extract-data.ts b/samples/typescript/src/scripts/extract-data.ts index 1e8d5c4..69922f1 100644 --- a/samples/typescript/src/scripts/extract-data.ts +++ b/samples/typescript/src/scripts/extract-data.ts @@ -1,39 +1,38 @@ #!/usr/bin/env node /** - * 📊 DOCUMENT DATA EXTRACTION - * ============================ - * - * The script exemplifies a typical workflow for intelligent document data extraction. - * As a data analyst, you need to extract structured data from PDF documents - whether - * form fields from applications, surveys, and questionnaires, or table data from - * reports, invoices, and financial statements. Manual data entry is error-prone and - * time-consuming, especially when processing hundreds of documents for analysis or - * database import. - * - * This workflow automates data extraction using AI-powered document understanding. - * The script analyzes PDF documents and intelligently identifies and extracts either - * form fields (with field names and values) or table structures (with rows, columns, - * and cell contents). The extracted data is saved as structured JSON, ready for - * immediate integration with databases, spreadsheets, or analytics pipelines. - * - * DATA EXTRACTION FEATURES: - * ✓ AI-powered form field extraction - * ✓ Intelligent table detection and extraction - * ✓ Structured JSON output format - * ✓ High accuracy recognition - * - * USAGE: - * npm run extract -- - * # or with tsx: - * tsx src/scripts/extract-data.ts - * - * MODES: - * forms - Extract form fields (name-value pairs) - * tables - Extract table data (rows and columns) - * - * EXAMPLES: - * npm run extract -- forms application.pdf data.json - * npm run extract -- tables invoice.pdf tables.json +📊 DOCUMENT DATA EXTRACTION +============================ + +The script exemplifies a typical workflow for intelligent document data extraction. +As a data analyst, you need to extract structured +data from PDF documents - whether form fields from applications, surveys, and +questionnaires, or table data from reports, invoices, and financial statements. +Manual data entry is error-prone and time-consuming, especially when processing +hundreds of documents for analysis or database import. + +This workflow automates data extraction using AI-powered document understanding. +The script analyzes PDF documents and intelligently identifies and extracts either +form fields (with field names and values) or table structures (with rows, columns, +and cell contents). The extracted data is saved as structured JSON, ready for +immediate integration with databases, spreadsheets, or analytics pipelines. + +DATA EXTRACTION FEATURES: + ✓ AI-powered form field extraction + ✓ Intelligent table detection and extraction + ✓ Structured JSON output format + ✓ High accuracy recognition + + +USAGE: + npm run extract -- + +MODES: + forms - Extract form fields (name-value pairs) + tables - Extract table data (rows and columns) + +EXAMPLES: + npm run extract -- forms application.pdf data.json + npm run extract -- tables invoice.pdf tables.json */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts index 52f9a22..6f9c76d 100644 --- a/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts +++ b/samples/typescript/src/scripts/prepare-pdf-for-distribution.ts @@ -1,35 +1,32 @@ #!/usr/bin/env node /** - * 🔒 PREPARE PDF FOR DISTRIBUTION - * ================================ - * - * The script exemplifies a typical workflow of marketing brochure distribution. - * As a marketing professional, it's necessary to share company brochures externally - * while ensuring they comply with corporate distribution standards. Word document - * properties can expose internal information such as author names, template paths, - * revision history, and company file structures that should remain confidential. - * - * This workflow automates compliant document preparation. The script processes each - * file individually - for every brochure in the input folder, it converts the Word - * document into PDF format, then compresses the file to reduce size and optimize - * transmission, and finally removes all metadata properties to ensure privacy and - * confidentiality. Each processed file is saved to the output folder, resulting in - * distribution-ready brochures. - * - * COMPANY DISTRIBUTION STANDARDS: - * ✓ PDF format (prevents editing) - * ✓ Compressed (optimized file size) - * ✓ Properties removed (no metadata exposure) - * ⏳ Annotations removed (feature in development) - * ⏳ Accessibility enabled (feature in development) - * - * USAGE: - * npm run prepare-pdf -- - * # or with tsx: - * tsx src/scripts/prepare-pdf-for-distribution.ts - * - * EXAMPLE: - * npm run prepare-pdf -- ../../test_files/test-batch ./output +🔒 PREPARE PDF FOR DISTRIBUTION +================================ + +The script exemplifies a typical workflow of marketing brochure distribution. +As a marketing professional, it's necessary to share company brochures externally +while ensuring they comply with corporate distribution standards. Word document +properties can expose internal information such as author names, template paths, +revision history, and company file structures that should remain confidential. + +This workflow automates compliant document preparation. The script processes each +file individually - for every brochure in the input folder, it converts the Word +document into PDF format, then compresses the file to reduce size and optimize +transmission, and finally removes all metadata properties to ensure privacy and +confidentiality. Each processed file is saved to the output folder, resulting in +distribution-ready brochures. + +COMPANY DISTRIBUTION STANDARDS: + ✓ PDF format (prevents editing) + ✓ Compressed (optimized file size) + ✓ Properties removed (no metadata exposure) + ⏳ Annotations removed (feature in development) + ⏳ Accessibility enabled (feature in development) +USAGE: + npm run prepare-pdf -- + +EXAMPLE: + npm run prepare-pdf -- ../../test_files/test-batch ./output */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/redact-by-keyword.ts b/samples/typescript/src/scripts/redact-by-keyword.ts index a7c1728..e23988a 100644 --- a/samples/typescript/src/scripts/redact-by-keyword.ts +++ b/samples/typescript/src/scripts/redact-by-keyword.ts @@ -1,38 +1,36 @@ #!/usr/bin/env node /** - * 🔍 KEYWORD-BASED REDACTION - * =========================== - * - * The script exemplifies a typical workflow for targeted content redaction. - * As a compliance officer, you need to redact specific sensitive terms from documents - * before external sharing or public disclosure. Whether removing client names, project - * codenames, financial figures, or proprietary terminology, manually searching through - * pages and applying redactions is tedious and risks missing instances, potentially - * exposing confidential information. - * - * This workflow automates keyword-based redaction. The script searches the entire - * PDF document for all specified keywords and phrases, identifies their exact - * locations across all pages, then automatically applies permanent redactions to - * remove them. Multiple keywords can be processed in a single pass, ensuring - * comprehensive coverage. The result is a thoroughly redacted document ready for - * safe distribution. - * - * KEYWORD REDACTION FEATURES: - * ✓ Multi-keyword search (process multiple terms) - * ✓ Whole document scanning (all pages) - * ✓ Exact location detection - * ✓ Permanent redaction (unrecoverable) - * ⏳ Case-insensitive matching (feature in development) - * ⏳ Regex pattern support (feature in development) - * - * USAGE: - * npm run redact-keyword -- [keyword2 ...] - * # or with tsx: - * tsx src/scripts/redact-by-keyword.ts [keyword2 ...] - * - * EXAMPLES: - * npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" - * npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" +🔍 KEYWORD-BASED REDACTION +=========================== + +The script exemplifies a typical workflow for targeted content redaction. +As a compliance officer, you need to redact specific +sensitive terms from documents before external sharing or public disclosure. +Whether removing client names, project codenames, financial figures, or +proprietary terminology, manually searching through pages and applying redactions +is tedious and risks missing instances, potentially exposing confidential information. + +This workflow automates keyword-based redaction. The script searches the entire +PDF document for all specified keywords and phrases, identifies their exact +locations across all pages, then automatically applies permanent redactions to +remove them. Multiple keywords can be processed in a single pass, ensuring +comprehensive coverage. The result is a thoroughly redacted document ready for +safe distribution. + +KEYWORD REDACTION FEATURES: + ✓ Multi-keyword search (process multiple terms) + ✓ Whole document scanning (all pages) + ✓ Exact location detection + ✓ Permanent redaction (unrecoverable) + ⏳ Case-insensitive matching (feature in development) + ⏳ Regex pattern support (feature in development) + +USAGE: + npm run redact-keyword -- [keyword2 ...] + +EXAMPLES: + npm run redact-keyword -- contract.pdf redacted.pdf "confidential" "proprietary" + npm run redact-keyword -- report.pdf clean.pdf "Project Zeus" "Client ABC" */ import { program } from 'commander'; diff --git a/samples/typescript/src/scripts/smart-redact-pii.ts b/samples/typescript/src/scripts/smart-redact-pii.ts index 7ab5da1..bbff0e3 100644 --- a/samples/typescript/src/scripts/smart-redact-pii.ts +++ b/samples/typescript/src/scripts/smart-redact-pii.ts @@ -1,38 +1,36 @@ #!/usr/bin/env node -/** - * 🔒 SMART PII REDACTION - * ====================== - * - * The script exemplifies a typical workflow for protecting sensitive customer information. - * As a compliance officer, it's essential to review and redact personally identifiable - * information (PII) from documents before sharing them with third parties, storing them - * in public systems, or using them for analysis. Manual redaction is time-consuming and - * error-prone, potentially missing sensitive data like social security numbers, phone - * numbers, addresses, or email addresses. - * - * This workflow automates compliant document redaction. The script processes each PDF - * file individually - for every document in the input folder, it uses AI-powered PII - * detection to identify all instances of sensitive information across all pages, then - * automatically applies redactions to permanently remove this data. Each processed file - * is saved to the output folder with all PII securely redacted, ready for safe sharing - * or archival. - * - * PRIVACY COMPLIANCE STANDARDS: - * ✓ AI-powered PII detection (SSN, phone, email, address) - * ✓ Automatic redaction (permanent removal) - * ✓ Batch processing (entire folders) - * - * USAGE: - * npm run smart-redact -- - * # or with tsx: - * tsx src/scripts/smart-redact-pii.ts - * - * EXAMPLE: - * npm run smart-redact -- ../../test_files/test-pdfs ./output +/** +🔒 SMART PII REDACTION +====================== + +The script exemplifies a typical workflow for protecting sensitive customer information. +As a compliance officer, it's essential to review and redact +personally identifiable information (PII) from documents before sharing them with +third parties, storing them in public systems, or using them for analysis. Manual +redaction is time-consuming and error-prone, potentially missing sensitive data like +social security numbers, phone numbers, addresses, or email addresses. + +This workflow automates compliant document redaction. The script processes each PDF +file individually - for every document in the input folder, it uses AI-powered PII +detection to identify all instances of sensitive information across all pages, then +automatically applies redactions to permanently remove this data. Each processed file +is saved to the output folder with all PII securely redacted, ready for safe sharing +or archival. + +PRIVACY COMPLIANCE STANDARDS: + ✓ AI-powered PII detection (SSN, phone, email, address) + ✓ Automatic redaction (permanent removal) + ✓ Batch processing (entire folders) + +USAGE: + npm run smart-redact -- + +EXAMPLE: + npm run smart-redact -- ../../test_files/test-pdfs ./output */ import { program } from 'commander'; -import { readFile, writeFile, copyFile } from 'fs/promises'; +import { writeFile, copyFile } from 'fs/promises'; import { join } from 'path'; import { PlatformAPIClient } from '../api/platform-api.js'; import { validateAndSetup } from '../helpers/document-helpers.js';