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 <noreply@anthropic.com>
This commit is contained in:
Dave Porter 2025-10-10 12:52:08 -04:00
commit ad0c512012
3 changed files with 1021 additions and 0 deletions

37
.gitignore vendored Normal file
View file

@ -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

496
README.md Normal file
View file

@ -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": "<p>PowerClean FurFinder + FurGuard 2025-related workflows</p>",
"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: `"<p>PowerClean FurFinder + FurGuard 2025-related workflows</p>"`
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_)

488
wrike_import.py Normal file
View file

@ -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("<p>", "").replace("</p>", "")
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 <json_directory>")
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()