Add LGL Team migration with recursive duplicate detection
- Add wrike_monitor_lgl.py for LGL Team board
* Updated API token and space configuration
* All 11 custom fields mapped correctly
* OMG# field now stores HTML links (matching LGL Team format)
* Recursive duplicate detection across entire Business Areas folder
* Handles HTML link extraction for accurate comparison
- Add discover_board_info.py
* Automated discovery tool for board configuration
* Finds space IDs, custom field IDs, and item types
* Generates configuration snippets
- Add config_lgl_team.py
* Reference configuration for LGL Team space
* Complete field mapping documentation
- Add test_duplicate_detection.py
* Testing tool to verify duplicate detection logic
* Can search for specific OMG# values
- Update requirements.txt
- Remove wrike_import.py (moved to OLD/)
Key Features:
- NO DUPLICATES: Searches entire Business Areas folder before creating
- HTML Link Support: OMG# stored as clickable links matching existing format
- Global Search: Uses descendants=true for efficient recursive search
- Format Matching: Generates OMG# links identical to existing entries
🤖 Generated with Claude Code
This commit is contained in:
parent
9f2b54f864
commit
a0cfe4b796
6 changed files with 1453 additions and 489 deletions
20
config_lgl_team.py
Normal file
20
config_lgl_team.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
# === WRIKE SPACE CONFIGURATION - LGL team ===
|
||||
WRIKE_SPACE_ID = "MQAAAABoHcTY"
|
||||
WRIKE_SPACE_NAME = "LGL team"
|
||||
DELIVERABLE_ITEM_TYPE_ID = "XXXXXX" # ⚠️ NOT FOUND - Update manually
|
||||
|
||||
# Custom field IDs
|
||||
CUSTOM_FIELDS = {
|
||||
"budget": "IEAGUQQ3JUAJCJ4N",
|
||||
"impact": "IEAGUQQ3JUAJRZ7Q",
|
||||
"notes": "IEAGUQQ3JUAKAJIW",
|
||||
"rag": "IEAGUQQ3JUAJRZ7S",
|
||||
"deliverable_category": "IEAGUQQ3JUAJLKMT",
|
||||
"actions": "IEAGUQQ3JUAJRZ7W",
|
||||
"shoot_date": "IEAGUQQ3JUAJRZ7X",
|
||||
"omg_number": "XXXXXX", # ⚠️ NOT FOUND - Update manually
|
||||
"omg_url": "IEAGUQQ3JUAKGGLP",
|
||||
"box_link": "IEAGUQQ3JUAJRZ7Z",
|
||||
"owner": "IEAGUQQ3JUAKAJIV",
|
||||
}
|
||||
229
discover_board_info.py
Normal file
229
discover_board_info.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Wrike Board Discovery Script
|
||||
Discovers all necessary IDs and configuration for a Wrike board/space
|
||||
to configure wrike_monitor.py for a new environment
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
class WrikeBoardDiscovery:
|
||||
"""Discover Wrike board configuration"""
|
||||
|
||||
def __init__(self, api_token: str):
|
||||
self.api_base = "https://www.wrike.com/api/v4"
|
||||
self.api_token = api_token
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def make_request(self, endpoint: str) -> Optional[Dict[str, Any]]:
|
||||
"""Make a GET request to Wrike API"""
|
||||
url = f"{self.api_base}{endpoint}"
|
||||
try:
|
||||
response = requests.get(url, headers=self.headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ API Error: {e}")
|
||||
return None
|
||||
|
||||
def get_all_spaces(self):
|
||||
"""Get all available spaces"""
|
||||
print("\n" + "="*80)
|
||||
print("DISCOVERING SPACES")
|
||||
print("="*80)
|
||||
|
||||
result = self.make_request("/spaces")
|
||||
if result and "data" in result:
|
||||
spaces = result["data"]
|
||||
print(f"\nFound {len(spaces)} space(s):\n")
|
||||
for space in spaces:
|
||||
print(f" • {space['title']}")
|
||||
print(f" ID: {space['id']}")
|
||||
print(f" Access Type: {space.get('accessType', 'N/A')}")
|
||||
print()
|
||||
return spaces
|
||||
return []
|
||||
|
||||
def get_space_by_name(self, space_name: str) -> Optional[str]:
|
||||
"""Get space ID by name"""
|
||||
result = self.make_request("/spaces")
|
||||
if result and "data" in result:
|
||||
for space in result["data"]:
|
||||
if space["title"].lower() == space_name.lower():
|
||||
return space["id"]
|
||||
return None
|
||||
|
||||
def get_custom_item_types(self, space_id: str):
|
||||
"""Get custom item types (like 'Deliverable')"""
|
||||
print("\n" + "="*80)
|
||||
print("DISCOVERING CUSTOM ITEM TYPES")
|
||||
print("="*80)
|
||||
|
||||
result = self.make_request(f"/spaces/{space_id}/item_types")
|
||||
if result and "data" in result:
|
||||
item_types = result["data"]
|
||||
print(f"\nFound {len(item_types)} custom item type(s):\n")
|
||||
for item_type in item_types:
|
||||
print(f" • {item_type['title']}")
|
||||
print(f" ID: {item_type['id']}")
|
||||
print(f" Type: {item_type.get('type', 'N/A')}")
|
||||
print()
|
||||
return item_types
|
||||
return []
|
||||
|
||||
def get_custom_fields(self):
|
||||
"""Get all custom fields"""
|
||||
print("\n" + "="*80)
|
||||
print("DISCOVERING CUSTOM FIELDS")
|
||||
print("="*80)
|
||||
|
||||
result = self.make_request("/customfields")
|
||||
if result and "data" in result:
|
||||
custom_fields = result["data"]
|
||||
print(f"\nFound {len(custom_fields)} custom field(s):\n")
|
||||
|
||||
# Group by account/space
|
||||
for field in custom_fields:
|
||||
print(f" • {field['title']}")
|
||||
print(f" ID: {field['id']}")
|
||||
print(f" Type: {field['type']}")
|
||||
if 'accountId' in field:
|
||||
print(f" Account ID: {field['accountId']}")
|
||||
if 'spaceId' in field:
|
||||
print(f" Space ID: {field['spaceId']}")
|
||||
print()
|
||||
|
||||
return custom_fields
|
||||
return []
|
||||
|
||||
def generate_config_snippet(self, space_name: str, space_id: str,
|
||||
deliverable_type_id: Optional[str],
|
||||
custom_fields: list):
|
||||
"""Generate Python config snippet for the discovered board"""
|
||||
print("\n" + "="*80)
|
||||
print("CONFIGURATION SNIPPET FOR wrike_monitor.py")
|
||||
print("="*80)
|
||||
|
||||
config = f'''
|
||||
# === WRIKE SPACE CONFIGURATION - {space_name} ===
|
||||
WRIKE_SPACE_ID = "{space_id}"
|
||||
WRIKE_SPACE_NAME = "{space_name}"
|
||||
'''
|
||||
|
||||
if deliverable_type_id:
|
||||
config += f'DELIVERABLE_ITEM_TYPE_ID = "{deliverable_type_id}" # Custom item type "Deliverable"\n'
|
||||
else:
|
||||
config += 'DELIVERABLE_ITEM_TYPE_ID = "XXXXXX" # ⚠️ NOT FOUND - Update manually\n'
|
||||
|
||||
config += '\n# Custom field IDs\n'
|
||||
config += 'CUSTOM_FIELDS = {\n'
|
||||
|
||||
# Try to match custom fields by name
|
||||
field_mapping = {
|
||||
"budget": None,
|
||||
"impact": None,
|
||||
"notes": None,
|
||||
"rag": None,
|
||||
"deliverable_category": None,
|
||||
"actions": None,
|
||||
"shoot_date": None,
|
||||
"omg_number": None,
|
||||
"omg_url": None,
|
||||
"box_link": None,
|
||||
"owner": None
|
||||
}
|
||||
|
||||
for field in custom_fields:
|
||||
field_title = field['title'].lower().replace(" ", "_").replace("#", "number")
|
||||
for key in field_mapping.keys():
|
||||
if key in field_title or field_title in key:
|
||||
field_mapping[key] = field['id']
|
||||
break
|
||||
|
||||
for key, value in field_mapping.items():
|
||||
if value:
|
||||
config += f' "{key}": "{value}",\n'
|
||||
else:
|
||||
config += f' "{key}": "XXXXXX", # ⚠️ NOT FOUND - Update manually\n'
|
||||
|
||||
config += '}\n'
|
||||
|
||||
print(config)
|
||||
|
||||
# Save to file
|
||||
output_file = f"config_{space_name.replace(' ', '_').lower()}.py"
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(config)
|
||||
|
||||
print(f"\n✅ Configuration saved to: {output_file}")
|
||||
|
||||
return config
|
||||
|
||||
def discover_board(self, space_name: str):
|
||||
"""Main discovery function for a specific board"""
|
||||
print(f"\n🔍 Discovering configuration for: {space_name}\n")
|
||||
|
||||
# Get all spaces
|
||||
spaces = self.get_all_spaces()
|
||||
|
||||
# Find target space
|
||||
space_id = self.get_space_by_name(space_name)
|
||||
if not space_id:
|
||||
print(f"\n❌ Space '{space_name}' not found!")
|
||||
print(f"Available spaces: {[s['title'] for s in spaces]}")
|
||||
return
|
||||
|
||||
print(f"\n✅ Found space: {space_name} (ID: {space_id})")
|
||||
|
||||
# Get custom item types
|
||||
item_types = self.get_custom_item_types(space_id)
|
||||
deliverable_type_id = None
|
||||
for item_type in item_types:
|
||||
if "deliverable" in item_type['title'].lower():
|
||||
deliverable_type_id = item_type['id']
|
||||
print(f"✅ Found 'Deliverable' custom item type: {deliverable_type_id}")
|
||||
break
|
||||
|
||||
if not deliverable_type_id:
|
||||
print("⚠️ WARNING: No 'Deliverable' custom item type found!")
|
||||
|
||||
# Get custom fields
|
||||
custom_fields = self.get_custom_fields()
|
||||
|
||||
# Generate config
|
||||
self.generate_config_snippet(space_name, space_id, deliverable_type_id, custom_fields)
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("DISCOVERY COMPLETE")
|
||||
print("="*80)
|
||||
print("\n📋 Next Steps:")
|
||||
print(" 1. Review the generated config file")
|
||||
print(" 2. Update any fields marked with ⚠️ XXXXXX")
|
||||
print(" 3. Copy the configuration into wrike_monitor.py")
|
||||
print(" 4. Test with a sample JSON file\n")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ WRIKE BOARD DISCOVERY TOOL ║
|
||||
║ Discovers IDs and configuration for Wrike monitor script ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# Configuration
|
||||
API_TOKEN = "eyJ0dCI6InAiLCJhbGciOiJIUzI1NiIsInR2IjoiMiJ9.eyJkIjoie1wiYVwiOjY5NjM3MzksXCJpXCI6OTU5MTM2MSxcImNcIjo0NzAyNjA1LFwidVwiOjIzMjcyNDA0LFwiclwiOlwiVVNcIixcInNcIjpbXCJXXCIsXCJGXCIsXCJJXCIsXCJVXCIsXCJLXCIsXCJDXCIsXCJEXCIsXCJNXCIsXCJBXCIsXCJMXCIsXCJQXCJdLFwielwiOltdLFwidFwiOjB9IiwiaWF0IjoxNzY2MDAwNjMyfQ.FwtNCIiBUbb82MlEnI_Z3vKt-W8IgKRwLwZY6IY6fEI"
|
||||
TARGET_SPACE = "LGL team" # Change this to discover a different space
|
||||
|
||||
discoverer = WrikeBoardDiscovery(API_TOKEN)
|
||||
discoverer.discover_board(TARGET_SPACE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Core dependencies (required for both scripts)
|
||||
pip# Core dependencies (required for both scripts)
|
||||
requests>=2.31.0
|
||||
|
||||
# Additional dependencies for wrike_monitor.py only
|
||||
|
|
|
|||
139
test_duplicate_detection.py
Normal file
139
test_duplicate_detection.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify duplicate detection works correctly
|
||||
This will check if a deliverable with a specific OMG# exists in the LGL Team board
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
# Configuration
|
||||
API_TOKEN = "eyJ0dCI6InAiLCJhbGciOiJIUzI1NiIsInR2IjoiMiJ9.eyJkIjoie1wiYVwiOjY5NjM3MzksXCJpXCI6OTU5MTM2MSxcImNcIjo0NzAyNjA1LFwidVwiOjIzMjcyNDA0LFwiclwiOlwiVVNcIixcInNcIjpbXCJXXCIsXCJGXCIsXCJJXCIsXCJVXCIsXCJLXCIsXCJDXCIsXCJEXCIsXCJNXCIsXCJBXCIsXCJMXCIsXCJQXCJdLFwielwiOltdLFwidFwiOjB9IiwiaWF0IjoxNzY2MDAwNjMyfQ.FwtNCIiBUbb82MlEnI_Z3vKt-W8IgKRwLwZY6IY6fEI"
|
||||
API_BASE = "https://www.wrike.com/api/v4"
|
||||
SPACE_ID = "MQAAAABoHcTY" # LGL Team
|
||||
OMG_NUMBER_FIELD_ID = "IEAGUQQ3JUAJL7YF"
|
||||
|
||||
def make_request(endpoint):
|
||||
"""Make a GET request to Wrike API"""
|
||||
url = f"{API_BASE}{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {API_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"❌ API Error: {e}")
|
||||
return None
|
||||
|
||||
def find_deliverable_by_omg(folder_id, omg_number):
|
||||
"""Find deliverable with matching OMG number"""
|
||||
print(f"\n🔍 Searching for deliverable with OMG# {omg_number} in folder {folder_id}...")
|
||||
|
||||
# Get all subfolders/projects
|
||||
result = make_request(f"/folders/{folder_id}/folders")
|
||||
|
||||
if result and "data" in result:
|
||||
# Find parent folder with childIds
|
||||
parent_folder = None
|
||||
for item in result["data"]:
|
||||
if item["id"] == folder_id:
|
||||
parent_folder = item
|
||||
break
|
||||
|
||||
if parent_folder:
|
||||
child_ids = parent_folder.get("childIds", [])
|
||||
print(f" Found {len(child_ids)} child items to check")
|
||||
|
||||
# For each child, get its details with custom fields
|
||||
for child_id in child_ids:
|
||||
child_result = make_request(f"/folders/{child_id}")
|
||||
if child_result and "data" in child_result and len(child_result["data"]) > 0:
|
||||
child = child_result["data"][0]
|
||||
|
||||
# Check if it's a project (deliverable)
|
||||
if "project" in child:
|
||||
custom_fields = child.get("customFields", [])
|
||||
for field in custom_fields:
|
||||
if field.get("id") == OMG_NUMBER_FIELD_ID:
|
||||
field_value = field.get("value")
|
||||
if field_value == omg_number:
|
||||
print(f" ✅ FOUND: '{child['title']}' (ID: {child_id}) has OMG# {field_value}")
|
||||
return child_id
|
||||
else:
|
||||
print(f" - '{child['title']}' has OMG# {field_value} (doesn't match)")
|
||||
|
||||
print(f" ❌ NOT FOUND: No deliverable with OMG# {omg_number}")
|
||||
return None
|
||||
|
||||
def list_recent_deliverables_with_omg(space_id):
|
||||
"""List some existing deliverables with their OMG numbers"""
|
||||
print(f"\n📋 Listing existing deliverables with OMG# in space {space_id}...")
|
||||
print("="*80)
|
||||
|
||||
# Get top-level folders in space
|
||||
result = make_request(f"/folders/{space_id}/folders")
|
||||
|
||||
if result and "data" in result:
|
||||
# Find space folder with childIds
|
||||
space_folder = None
|
||||
for item in result["data"]:
|
||||
if item["id"] == space_id:
|
||||
space_folder = item
|
||||
break
|
||||
|
||||
if space_folder:
|
||||
child_ids = space_folder.get("childIds", [])[:5] # Limit to first 5 folders
|
||||
|
||||
for folder_id in child_ids:
|
||||
folder_result = make_request(f"/folders/{folder_id}/folders")
|
||||
|
||||
if folder_result and "data" in folder_result:
|
||||
for item in folder_result["data"]:
|
||||
if "project" in item:
|
||||
custom_fields = item.get("customFields", [])
|
||||
for field in custom_fields:
|
||||
if field.get("id") == OMG_NUMBER_FIELD_ID:
|
||||
omg_value = field.get("value", "N/A")
|
||||
print(f" • {item['title']}")
|
||||
print(f" OMG#: {omg_value}")
|
||||
print(f" ID: {item['id']}")
|
||||
print()
|
||||
|
||||
def main():
|
||||
print("""
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ DUPLICATE DETECTION TEST ║
|
||||
║ Verifies that the script can find existing deliverables ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
# List some existing deliverables
|
||||
list_recent_deliverables_with_omg(SPACE_ID)
|
||||
|
||||
# Test duplicate detection
|
||||
print("\n" + "="*80)
|
||||
print("TESTING DUPLICATE DETECTION")
|
||||
print("="*80)
|
||||
|
||||
# You can test with a known OMG# from the list above
|
||||
test_omg = input("\nEnter an OMG# to test (or press Enter to skip): ").strip()
|
||||
|
||||
if test_omg:
|
||||
# You'll need to provide a folder ID to search in
|
||||
test_folder = input("Enter the folder/project ID to search in: ").strip()
|
||||
|
||||
if test_folder:
|
||||
result = find_deliverable_by_omg(test_folder, test_omg)
|
||||
|
||||
if result:
|
||||
print(f"\n✅ SUCCESS: Duplicate detection would catch this!")
|
||||
print(f" The script would SKIP creating a new deliverable")
|
||||
print(f" and use existing ID: {result}")
|
||||
else:
|
||||
print(f"\n✅ SUCCESS: OMG# {test_omg} not found")
|
||||
print(f" The script would CREATE a new deliverable")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
488
wrike_import.py
488
wrike_import.py
|
|
@ -1,488 +0,0 @@
|
|||
#!/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()
|
||||
1064
wrike_monitor_lgl.py
Normal file
1064
wrike_monitor_lgl.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue