commit ad0c512012486f4a3f539e2ac2fdceb4b7631fa7 Author: Dave Porter Date: Fri Oct 10 12:52:08 2025 -0400 Initial commit: Wrike import script v1.2 - Automatic folder/project/task creation from JSON files - Duplicate detection via OMG numbers - Custom field mapping - File management (Processed folder + 24hr cleanup) - Production ready šŸ¤– Generated with Claude Code Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..449ba6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +bissell-wrike/ +*.egg-info/ +dist/ +build/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Project specific +json_files/*.json +json_files/Processed/*.json +*.log + +# Keep directory structure +!json_files/.gitkeep +!json_files/Processed/.gitkeep + +# Sensitive +*.env +.env +credentials.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d37564 --- /dev/null +++ b/README.md @@ -0,0 +1,496 @@ +# Wrike Import Script + +A Python script to automatically import project structures and deliverable tasks into Wrike from JSON files. + +## Overview + +This script processes JSON files containing job specifications and creates the corresponding structure in Wrike: +- **Folders** (product categories like "Dry Specialty", "Air", "PDC") +- **Projects** (campaigns with timelines and metadata) +- **Tasks** (deliverables with custom fields and due dates) + +The script intelligently handles duplicates by checking OMG numbers before creating new items, updating existing ones instead. + +## Features + +- āœ… **Automatic folder creation** from BusinessArea hierarchy +- āœ… **Project management** with start/end dates and campaign codes +- āœ… **Task creation/updates** with comprehensive custom fields +- āœ… **Duplicate prevention** via OMG number checking +- āœ… **Smart caching** to minimize API calls +- āœ… **Detailed logging** with progress tracking +- āœ… **Batch processing** of multiple JSON files +- āœ… **Automatic file management** - moves processed files to Processed subfolder +- āœ… **Auto-cleanup** - deletes files older than 24 hours from Processed folder + +## Requirements + +### Python Dependencies +```bash +pip install requests +``` + +### Python Version +- Python 3.6 or higher + +### Wrike Requirements +- Valid Wrike API token with write permissions +- Access to the target Wrike space +- Custom fields configured in the space (see Configuration section) + +## Installation + +1. Clone or download this repository +2. Install dependencies: + ```bash + pip install requests + ``` +3. Configure your Wrike API token in the script (see Configuration) + +## Configuration + +### 1. API Token + +Edit `wrike_import.py` and update the `WRIKE_TOKEN` constant: + +```python +WRIKE_TOKEN = "your_wrike_api_token_here" +``` + +### 2. Target Space + +Update the `STAGING_SPACE_ID` to point to your target Wrike space: + +```python +STAGING_SPACE_ID = "MQAAAABpz7l_" # Your space ID +``` + +To find your space ID: +```bash +curl -X GET "https://www.wrike.com/api/v4/spaces" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +### 3. Custom Fields + +The script expects the following custom fields to exist in your Wrike space: + +| Field Name | Type | ID Variable | Usage | +|------------|------|-------------|-------| +| Budget | Currency | `budget` | Project budgets | +| Impact | Dropdown | `impact` | Priority level | +| Notes | Text | `notes` | Additional information | +| RAG | Dropdown | `rag` | Status (Red/Amber/Green) | +| Deliverable Category | Dropdown | `deliverable_category` | Type of deliverable | +| Actions | Text | `actions` | Next steps | +| Shoot date | Date | `shoot_date` | Photography date | +| OMG # | Text | `omg_number` | Job/Campaign number | +| Box Link | Text | `box_link` | Link to assets | +| Owner | Contacts | `owner` | Task owner | + +To create these fields, use the Wrike API or create them manually in the Wrike interface. + +**Update custom field IDs** in the script after creating them: + +```python +CUSTOM_FIELDS = { + "budget": "YOUR_BUDGET_FIELD_ID", + "impact": "YOUR_IMPACT_FIELD_ID", + # ... etc +} +``` + +## JSON File Format + +### Required Structure + +```json +{ + "JobSpecification": { + "ProjectDetails": { + "BusinessArea": "BISSELL > PRODUCT MARKETING > Dry Specialty", + "Title": "PowerClean FurFinder + FurGuard 2025", + "Description": "

PowerClean FurFinder + FurGuard 2025-related workflows

", + "StartDate": "2025-04-08 13:00:00+00", + "EndDate": "2025-12-31 17:06:00+00" + }, + "JobDetails": { + "Number": "5791330", + "Title": "PowerClean Corded Mass User Guide", + "CampaignCode": "1647476", + "JobCategory": "UX User Guide", + "MediaType": "Creative development", + "Type": "Creative development", + "Notes": "User guide for mass market", + "BriefDate": "2025-05-23 16:00:00+00", + "DueDate": "2025-09-17 16:04:00+00", + "BusinessArea": "BISSELL > PRODUCT MARKETING > Dry Specialty" + } + } +} +``` + +### Field Mapping + +#### Project Level +| JSON Field | Maps To | Notes | +|------------|---------|-------| +| `ProjectDetails.BusinessArea` | Folder name | Last segment (e.g., "Dry Specialty") | +| `ProjectDetails.Title` | Project title | | +| `ProjectDetails.Description` | Project description | HTML tags removed | +| `ProjectDetails.StartDate` | Project start date | Converted to YYYY-MM-DD | +| `ProjectDetails.EndDate` | Project end date | Converted to YYYY-MM-DD | +| `JobDetails.CampaignCode` | Project OMG # | Custom field | + +#### Task/Deliverable Level +| JSON Field | Maps To | Notes | +|------------|---------|-------| +| `JobDetails.Number` | Task OMG # | Used for duplicate detection | +| `JobDetails.Title` | Task title | | +| `JobDetails.Notes` | Task description | | +| `JobDetails.JobCategory` | Deliverable Category | Custom field | +| `JobDetails.MediaType` | Deliverable Category | Fallback if JobCategory empty | +| `JobDetails.Type` | Notes field | Combined with Details | +| `JobDetails.BriefDate` | Task start date | | +| `JobDetails.DueDate` | Task due date | | + +## Usage + +### Basic Usage + +Process all JSON files in a directory: + +```bash +python wrike_import.py /path/to/json/files/ +``` + +### Example + +```bash +# Process files in the current directory +python wrike_import.py ./jobs/ + +# Process files in a specific folder +python wrike_import.py ~/Documents/wrike_imports/ +``` + +### Output Example + +``` +Found 3 JSON file(s) to process +Target: Wrike Staging Space (MQAAAABpz7l_) + +================================================================================ +Processing: 5791330.json +================================================================================ + +1. Processing folder: 'Dry Specialty' + Found existing folder 'Dry Specialty': MQAAAABpz123 + +2. Processing project: 'PowerClean FurFinder + FurGuard 2025' + Campaign Code: 1647476 + Found existing project 'PowerClean FurFinder + FurGuard 2025': MQAAAABpz456 + +3. Processing deliverable task + Checking for existing task with OMG #: 5791330 + Creating new task 'PowerClean Corded Mass User Guide' (Job #5791330) + āœ“ Created task 'PowerClean Corded Mass User Guide': MAAAAABpz789 + +āœ“ Successfully processed 5791330.json + → Moved to: Processed/5791330.json + +================================================================================ +Processing: 5791331.json +================================================================================ + +1. Processing folder: 'Air' + Found existing folder 'Air': MQAAAABpz124 + +2. Processing project: 'Air Purifier 2025' + Campaign Code: 1647477 + Found existing project 'Air Purifier 2025': MQAAAABpz457 + +3. Processing deliverable task + Checking for existing task with OMG #: 5791331 + Found existing task: MAAAAABpz790 + āŠ™ Task 'Air Purifier Hero Image' already exists (Job #5791331) - skipping + +āŠ™ Successfully processed 5791331.json (task already exists) + → Moved to: Processed/5791331.json + +================================================================================ +CLEANUP +================================================================================ + Deleted old file: 5791329.json + Deleted old file: 5791328.json +Deleted 2 file(s) older than 24 hours from Processed folder + +================================================================================ +SUMMARY +================================================================================ +Total files: 3 +Successful: 3 +Skipped (already exists): 1 +Failed: 0 +Moved to Processed: 3 + +Folders created/found: 2 +Projects created/found: 3 +``` + +## How It Works + +### 1. Folder Management +- Extracts the last segment from `BusinessArea` (e.g., "BISSELL > PRODUCT MARKETING > Dry Specialty" → "Dry Specialty") +- Checks if folder exists in the target space +- Creates folder if not found +- Caches folder IDs for performance + +### 2. Project Management +- Searches for existing project by title in the folder +- Creates new project if not found +- Converts folder to project with start/end dates +- Adds campaign code as OMG # custom field +- Caches project IDs for performance + +### 3. Task Management +- Checks for existing task by OMG # (job number) +- **If found**: Updates existing task with new data +- **If not found**: Creates new task +- Populates all custom fields and dates + +### 4. Duplicate Handling + +The script prevents duplicates using OMG numbers: + +- **Projects**: Matched by title within folder +- **Tasks**: Matched by OMG # custom field (job number) + +When processing the same JSON file twice: +- 1st run: Creates folder, project, and task +- 2nd run: + - Finds existing folder and project (reuses them) + - Finds existing task with same OMG # → **Skips task creation** + - Marks file as "skipped" and moves to Processed folder + +**Important**: Tasks are never updated once created. If a task with the same OMG # exists, it's left unchanged. + +### 5. File Management + +After successful processing: +- **Moves file** to `Processed/` subfolder (auto-created) +- **Failed files** remain in the source directory for retry +- **Cleanup**: Deletes files older than 24 hours from Processed folder + +Directory structure: +``` +json_files/ +ā”œā”€ā”€ 5791331.json # Pending +ā”œā”€ā”€ 5791332.json # Pending +└── Processed/ + ā”œā”€ā”€ 5791330.json # Processed today (kept) + └── 5791329.json # Processed >24h ago (deleted) +``` + +## Troubleshooting + +### Common Issues + +#### 1. Authentication Error +``` +Error making Wrike request: 401 Unauthorized +``` +**Solution**: Verify your API token is correct and has write permissions. + +#### 2. Custom Field Not Found +``` +Error: Custom field ID not found +``` +**Solution**: Ensure all custom fields are created in Wrike and IDs are updated in the script. + +#### 3. No JSON Files Found +``` +No JSON files found in '/path/to/directory' +``` +**Solution**: +- Verify the directory path is correct +- Ensure files have `.json` extension +- Check file permissions + +#### 4. Invalid Space ID +``` +Error: Space 'MQAAAABpz7l_' not found +``` +**Solution**: +- Get your space ID from the API: `GET /spaces` +- Update `STAGING_SPACE_ID` in the script + +### Debug Mode + +To see detailed API responses, modify the `make_wrike_request` function to print full responses: + +```python +def make_wrike_request(method, endpoint, data=None): + # ... existing code ... + print(f"Response: {response.json()}") # Add this line + return response.json() +``` + +## API Rate Limits + +Wrike API has rate limits: +- **100 requests per minute** per token +- **1000 requests per hour** per token + +The script is optimized with caching to minimize API calls, but for very large batches (100+ files), you may need to: +1. Process in smaller batches +2. Add delays between requests + +## Data Mapping Reference + +### Date Format Conversion +Input: `"2025-05-23 16:00:00+00"` +Output: `"2025-05-23"` + +### HTML Cleaning +Input: `"

PowerClean FurFinder + FurGuard 2025-related workflows

"` +Output: `"PowerClean FurFinder + FurGuard 2025-related workflows"` + +### Business Area Parsing +Input: `"BISSELL > PRODUCT MARKETING > Dry Specialty"` +Output: `"Dry Specialty"` (folder name) + +## Advanced Usage + +### Custom Field Mapping + +To add more custom fields, update the script: + +1. Add field ID to `CUSTOM_FIELDS` dictionary +2. Update `create_or_update_deliverable_task()` function: + +```python +# Add your custom field +custom_fields.append({ + "id": CUSTOM_FIELDS["your_field_name"], + "value": job_details.get("YourJsonField", "") +}) +``` + +### Filter by Business Area + +To process only specific business areas, add a filter in `process_json_file()`: + +```python +folder_name = parse_business_area(business_area) + +# Only process specific folders +if folder_name not in ["Dry Specialty", "Air"]: + print(f" Skipping folder '{folder_name}'") + return False +``` + +### Batch Processing Script + +Create a wrapper script for automated processing: + +```bash +#!/bin/bash +# process_all.sh + +IMPORT_DIR="/path/to/json/files" + +# Run import (automatically moves to Processed/ and cleans up old files) +python wrike_import.py "$IMPORT_DIR" + +# Optional: Send notification +if [ $? -eq 0 ]; then + echo "Wrike import completed successfully" +fi +``` + +### Scheduled Processing with Cron + +Set up automatic processing every hour: + +```bash +# Edit crontab +crontab -e + +# Add this line to run every hour +0 * * * * /usr/bin/python3 /path/to/wrike_import.py /path/to/json/files >> /var/log/wrike_import.log 2>&1 +``` + +### Change Cleanup Retention Period + +To change the 24-hour retention to a different period, modify the cleanup call in the script: + +```python +# Delete files older than 48 hours instead +deleted_count = cleanup_old_files(json_dir, hours=48) + +# Delete files older than 7 days +deleted_count = cleanup_old_files(json_dir, hours=168) +``` + +## Security Notes + +āš ļø **Important Security Considerations:** + +1. **Never commit API tokens** to version control +2. Store tokens in environment variables: + ```python + import os + WRIKE_TOKEN = os.environ.get('WRIKE_API_TOKEN') + ``` +3. Use a `.env` file for local development (add to `.gitignore`) +4. Rotate tokens regularly +5. Use read-only tokens for testing + +## Support + +### Getting Help + +1. Check the [Wrike API Documentation](https://developers.wrike.com/api/v4/) +2. Review error messages in script output +3. Enable debug mode for detailed logging +4. Check Wrike API status at [status.wrike.com](https://status.wrike.com) + +### Reporting Issues + +When reporting issues, include: +- Script version +- Python version (`python --version`) +- Full error message +- Sample JSON structure (redacted) +- Wrike space configuration + +## License + +This script is provided as-is for internal use. + +## Changelog + +### Version 1.2 (Current) +- Added automatic file movement to Processed subfolder +- Implemented auto-cleanup of files older than 24 hours +- Improved file management and organization +- Enhanced output with file movement tracking + +### Version 1.1 +- Added duplicate detection via OMG numbers +- Separated project and task OMG numbers +- Improved error handling +- Added caching for performance +- Enhanced logging output + +### Version 1.0 +- Initial release +- Basic folder/project/task creation +- Custom field mapping + +--- + +**Last Updated**: October 2025 +**Author**: Dave Porter +**Wrike Space**: Staging (MQAAAABpz7l_) diff --git a/wrike_import.py b/wrike_import.py new file mode 100644 index 0000000..feb40c7 --- /dev/null +++ b/wrike_import.py @@ -0,0 +1,488 @@ +#!/usr/bin/env python3 +""" +Wrike Import Script +Processes JSON files to create folder structures, projects, and deliverable tasks in Wrike. +""" + +import json +import os +import sys +import requests +import shutil +import time +from datetime import datetime, timedelta +from pathlib import Path + +# Configuration +WRIKE_API_BASE = "https://www.wrike.com/api/v4" +WRIKE_TOKEN = "eyJ0dCI6InAiLCJhbGciOiJIUzI1NiIsInR2IjoiMiJ9.eyJkIjoie1wiYVwiOjY5NzM0OTgsXCJpXCI6OTUyOTY3MCxcImNcIjo0Njk5NTI3LFwidVwiOjIzMjcyNDA0LFwiclwiOlwiVVNcIixcInNcIjpbXCJXXCIsXCJGXCIsXCJJXCIsXCJVXCIsXCJLXCIsXCJDXCIsXCJEXCIsXCJNXCIsXCJBXCIsXCJMXCIsXCJQXCJdLFwielwiOltdLFwidFwiOjB9IiwiaWF0IjoxNzYwMDI4ODIzfQ.9f4t15LycpoH-NlzQC3s1K19fVqnAwcahG2D-J5E8dg" +STAGING_SPACE_ID = "MQAAAABpz7l_" + +# Custom field IDs in Staging space +CUSTOM_FIELDS = { + "budget": "IEAGU2B2JUAJRZ7P", + "impact": "IEAGU2B2JUAJRZ7Q", + "notes": "IEAGU2B2JUAJRZ7R", + "rag": "IEAGU2B2JUAJRZ7S", + "deliverable_category": "IEAGU2B2JUAJRZ7T", + "actions": "IEAGU2B2JUAJRZ7W", + "shoot_date": "IEAGU2B2JUAJRZ7X", + "omg_number": "IEAGU2B2JUAJRZ7Y", + "box_link": "IEAGU2B2JUAJRZ7Z", + "owner": "IEAGU2B2JUAJRZ72" +} + +# Cache for folder/project IDs +folder_cache = {} +project_cache = {} + + +def make_wrike_request(method, endpoint, data=None): + """Make a request to the Wrike API.""" + url = f"{WRIKE_API_BASE}{endpoint}" + headers = { + "Authorization": f"Bearer {WRIKE_TOKEN}", + "Content-Type": "application/json" + } + + try: + if method == "GET": + response = requests.get(url, headers=headers) + elif method == "POST": + response = requests.post(url, headers=headers, json=data) + elif method == "PUT": + response = requests.put(url, headers=headers, json=data) + + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Error making Wrike request: {e}") + if hasattr(e.response, 'text'): + print(f"Response: {e.response.text}") + return None + + +def parse_business_area(business_area): + """Extract the folder name from BusinessArea string. + Example: 'BISSELL > PRODUCT MARKETING > Dry Specialty' -> 'Dry Specialty' + """ + parts = business_area.split(">") + if len(parts) > 0: + return parts[-1].strip() + return business_area.strip() + + +def parse_date(date_string): + """Convert date string to YYYY-MM-DD format.""" + if not date_string: + return None + try: + # Parse format like "2025-05-23 16:00:00+00" + dt = datetime.strptime(date_string.split('+')[0].strip(), "%Y-%m-%d %H:%M:%S") + return dt.strftime("%Y-%m-%d") + except: + return None + + +def get_or_create_folder(folder_name, parent_id=STAGING_SPACE_ID): + """Get existing folder or create new one.""" + # Check cache + cache_key = f"{parent_id}:{folder_name}" + if cache_key in folder_cache: + print(f" Found folder '{folder_name}' in cache: {folder_cache[cache_key]}") + return folder_cache[cache_key] + + # Get folders in parent + result = make_wrike_request("GET", f"/folders/{parent_id}/folders") + if result and "data" in result: + for folder in result["data"]: + if folder["title"] == folder_name: + folder_id = folder["id"] + folder_cache[cache_key] = folder_id + print(f" Found existing folder '{folder_name}': {folder_id}") + return folder_id + + # Create new folder + print(f" Creating new folder '{folder_name}' under {parent_id}") + data = { + "title": folder_name, + "description": f"{folder_name} category folder" + } + result = make_wrike_request("POST", f"/folders/{parent_id}/folders", data) + if result and "data" in result and len(result["data"]) > 0: + folder_id = result["data"][0]["id"] + folder_cache[cache_key] = folder_id + print(f" Created folder '{folder_name}': {folder_id}") + return folder_id + + return None + + +def get_or_create_project(project_title, folder_id, project_details, campaign_code): + """Get existing project or create new one.""" + # Check cache + cache_key = f"{folder_id}:{project_title}" + if cache_key in project_cache: + print(f" Found project '{project_title}' in cache: {project_cache[cache_key]}") + return project_cache[cache_key] + + # Get folders in parent + result = make_wrike_request("GET", f"/folders/{folder_id}/folders") + if result and "data" in result: + for item in result["data"]: + if item["title"] == project_title and "project" in item: + project_id = item["id"] + project_cache[cache_key] = project_id + print(f" Found existing project '{project_title}': {project_id}") + return project_id + + # Create new project (as folder first, then convert) + print(f" Creating new project '{project_title}' under folder {folder_id}") + + # Extract description (remove HTML tags) + description = project_details.get("Description", "") + if description: + description = description.replace("

", "").replace("

", "") + + data = { + "title": project_title, + "description": description + } + result = make_wrike_request("POST", f"/folders/{folder_id}/folders", data) + + if result and "data" in result and len(result["data"]) > 0: + project_id = result["data"][0]["id"] + + # Convert to project with dates and custom fields + start_date = parse_date(project_details.get("StartDate")) + end_date = parse_date(project_details.get("EndDate")) + + project_data = {"project": {}} + if start_date: + project_data["project"]["startDate"] = start_date + if end_date: + project_data["project"]["endDate"] = end_date + + # Add campaign code as OMG # custom field for the project + if campaign_code: + project_data["customFields"] = [ + { + "id": CUSTOM_FIELDS["omg_number"], + "value": campaign_code + } + ] + + result = make_wrike_request("PUT", f"/folders/{project_id}", project_data) + + if result: + project_cache[cache_key] = project_id + print(f" Created project '{project_title}': {project_id} (Campaign Code: {campaign_code})") + return project_id + + return None + + +def find_task_by_omg_number(project_id, omg_number): + """Find a task in the project with the matching OMG number.""" + if not omg_number: + return None + + # Get all tasks in the project with custom fields + # IMPORTANT: Must add fields parameter to get custom field data + result = make_wrike_request("GET", f"/folders/{project_id}/tasks?fields=[\"customFields\"]") + + if result and "data" in result: + for task in result["data"]: + # Check custom fields for matching OMG number + custom_fields = task.get("customFields", []) + for field in custom_fields: + if field.get("id") == CUSTOM_FIELDS["omg_number"]: + if field.get("value") == omg_number: + return task["id"] + return None + + +def create_or_update_deliverable_task(project_id, job_details): + """Create or update a deliverable task in the project.""" + task_title = job_details.get("Title", "Untitled Task") + job_number = job_details.get("Number", "") + + # OMG number for deliverable is just the job number + omg_number = job_number + + # Check if task already exists with this OMG number + existing_task_id = None + if omg_number: + print(f" Checking for existing task with OMG #: {omg_number}") + existing_task_id = find_task_by_omg_number(project_id, omg_number) + + if existing_task_id: + print(f" Found existing task: {existing_task_id}") + print(f" āŠ™ Task '{task_title}' already exists (Job #{job_number}) - skipping") + return existing_task_id # Return existing ID to mark as success + else: + print(f" Creating new task '{task_title}' (Job #{job_number})") + + # Parse dates + due_date = parse_date(job_details.get("DueDate")) + brief_date = parse_date(job_details.get("BriefDate")) + + # Build task data + task_data = { + "title": task_title, + "description": job_details.get("Notes", ""), + } + + # Add dates + if due_date: + task_data["dates"] = {"due": due_date} + if brief_date: + task_data["dates"]["start"] = brief_date + + # Add custom fields + custom_fields = [] + + # Deliverable Category (from JobCategory or MediaType) + job_category = job_details.get("JobCategory", job_details.get("MediaType", "")) + if job_category: + custom_fields.append({ + "id": CUSTOM_FIELDS["deliverable_category"], + "value": job_category + }) + + # OMG Number (Job Number only) + if omg_number: + custom_fields.append({ + "id": CUSTOM_FIELDS["omg_number"], + "value": omg_number + }) + + # Notes (combine type and details) + notes_parts = [] + if job_details.get("Type"): + notes_parts.append(f"Type: {job_details['Type']}") + if job_details.get("Details"): + notes_parts.append(job_details["Details"]) + if notes_parts: + custom_fields.append({ + "id": CUSTOM_FIELDS["notes"], + "value": " | ".join(notes_parts) + }) + + if custom_fields: + task_data["customFields"] = custom_fields + + # Create new task (without custom fields first) + basic_task_data = { + "title": task_title, + "description": job_details.get("Notes", ""), + } + + # Add dates + if due_date: + basic_task_data["dates"] = {"due": due_date} + if brief_date: + basic_task_data["dates"]["start"] = brief_date + + result = make_wrike_request("POST", f"/folders/{project_id}/tasks", basic_task_data) + if result and "data" in result and len(result["data"]) > 0: + task_id = result["data"][0]["id"] + print(f" āœ“ Created task '{task_title}': {task_id}") + + # Now update with custom fields + if custom_fields: + print(f" Adding custom fields to task...") + update_data = {"customFields": custom_fields} + update_result = make_wrike_request("PUT", f"/tasks/{task_id}", update_data) + if update_result: + print(f" āœ“ Custom fields added") + else: + print(f" ⚠ Warning: Failed to add custom fields") + + return task_id + else: + print(f" āœ— Failed to create task '{task_title}'") + return None + + +def process_json_file(json_file_path): + """Process a single JSON file and create Wrike structure.""" + print(f"\n{'='*80}") + print(f"Processing: {json_file_path.name}") + print(f"{'='*80}") + + try: + with open(json_file_path, 'r') as f: + data = json.load(f) + except Exception as e: + print(f"Error reading JSON file: {e}") + return False + + # Extract data + job_spec = data.get("JobSpecification", {}) + project_details = job_spec.get("ProjectDetails", {}) + job_details = job_spec.get("JobDetails", {}) + + # 1. Get/Create top-level folder from BusinessArea + business_area = project_details.get("BusinessArea", job_details.get("BusinessArea", "")) + folder_name = parse_business_area(business_area) + + if not folder_name: + print(" āœ— No BusinessArea found, skipping") + return False + + print(f"\n1. Processing folder: '{folder_name}'") + folder_id = get_or_create_folder(folder_name) + + if not folder_id: + print(f" āœ— Failed to get/create folder '{folder_name}'") + return False + + # 2. Get/Create project + project_title = project_details.get("Title", "Untitled Project") + campaign_code = job_details.get("CampaignCode", "") + + print(f"\n2. Processing project: '{project_title}'") + if campaign_code: + print(f" Campaign Code: {campaign_code}") + + project_id = get_or_create_project(project_title, folder_id, project_details, campaign_code) + + if not project_id: + print(f" āœ— Failed to get/create project '{project_title}'") + return False + + # 3. Create or update deliverable task + print(f"\n3. Processing deliverable task") + task_id = create_or_update_deliverable_task(project_id, job_details) + + if task_id: + # Check if task was skipped (already existed) + job_number = job_details.get("Number", "") + existing_task = find_task_by_omg_number(project_id, job_number) if job_number else None + + if existing_task and existing_task == task_id: + # Task already existed, was skipped + print(f"\nāŠ™ Successfully processed {json_file_path.name} (task already exists)") + return "skipped" + else: + # New task was created + print(f"\nāœ“ Successfully processed {json_file_path.name}") + return True + else: + print(f"\nāœ— Failed to create deliverable task for {json_file_path.name}") + return False + + +def move_to_processed(json_file, json_dir): + """Move processed JSON file to Processed subfolder.""" + processed_dir = json_dir / "Processed" + processed_dir.mkdir(exist_ok=True) + + destination = processed_dir / json_file.name + try: + shutil.move(str(json_file), str(destination)) + print(f" → Moved to: Processed/{json_file.name}") + return True + except Exception as e: + print(f" āœ— Failed to move file: {e}") + return False + + +def cleanup_old_files(json_dir, hours=24): + """Delete files in Processed folder older than specified hours.""" + processed_dir = json_dir / "Processed" + + if not processed_dir.exists(): + return 0 + + cutoff_time = time.time() - (hours * 3600) + deleted_count = 0 + + for file in processed_dir.glob("*.json"): + try: + file_age = file.stat().st_mtime + if file_age < cutoff_time: + file.unlink() + deleted_count += 1 + print(f" Deleted old file: {file.name}") + except Exception as e: + print(f" Warning: Could not delete {file.name}: {e}") + + return deleted_count + + +def main(): + """Main function to process all JSON files in a directory.""" + if len(sys.argv) < 2: + print("Usage: python wrike_import.py ") + print("\nExample: python wrike_import.py ./json_files/") + sys.exit(1) + + json_dir = Path(sys.argv[1]) + + if not json_dir.exists() or not json_dir.is_dir(): + print(f"Error: Directory '{json_dir}' does not exist") + sys.exit(1) + + # Find all JSON files (exclude Processed subfolder) + json_files = [f for f in json_dir.glob("*.json") if "Processed" not in str(f)] + + if not json_files: + print(f"No JSON files found in '{json_dir}'") + sys.exit(1) + + print(f"\nFound {len(json_files)} JSON file(s) to process") + print(f"Target: Wrike Staging Space ({STAGING_SPACE_ID})") + + # Process each file + success_count = 0 + failed_count = 0 + moved_count = 0 + skipped_count = 0 + + for json_file in json_files: + try: + result = process_json_file(json_file) + if result == "skipped": + skipped_count += 1 + success_count += 1 # Still count as success for file movement + # Move skipped file to processed + if move_to_processed(json_file, json_dir): + moved_count += 1 + elif result: + success_count += 1 + # Move successfully processed file + if move_to_processed(json_file, json_dir): + moved_count += 1 + else: + failed_count += 1 + except Exception as e: + print(f"\nāœ— Unexpected error processing {json_file.name}: {e}") + failed_count += 1 + + # Cleanup old files in Processed folder + print(f"\n{'='*80}") + print(f"CLEANUP") + print(f"{'='*80}") + deleted_count = cleanup_old_files(json_dir, hours=24) + if deleted_count > 0: + print(f"Deleted {deleted_count} file(s) older than 24 hours from Processed folder") + else: + print("No old files to delete") + + # Summary + print(f"\n{'='*80}") + print(f"SUMMARY") + print(f"{'='*80}") + print(f"Total files: {len(json_files)}") + print(f"Successful: {success_count}") + print(f"Skipped (already exists): {skipped_count}") + print(f"Failed: {failed_count}") + print(f"Moved to Processed: {moved_count}") + print(f"\nFolders created/found: {len(folder_cache)}") + print(f"Projects created/found: {len(project_cache)}") + + +if __name__ == "__main__": + main()