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:
commit
ad0c512012
3 changed files with 1021 additions and 0 deletions
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
496
README.md
Normal 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
488
wrike_import.py
Normal 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()
|
||||
Loading…
Add table
Reference in a new issue