Fix lint issues across link building files
- Remove f-prefix from strings with no placeholders - Use list unpacking instead of concatenation - Fix import sorting in test file - Remove unused Path import - Use contextlib.suppress instead of try/except/pass - Wrap long lines to stay under 100 chars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
0f4c77adc9
commit
a1fc5a7c0f
|
|
@ -208,22 +208,22 @@ def main():
|
||||||
task_type_field_name=config.clickup.task_type_field_name,
|
task_type_field_name=config.clickup.task_type_field_name,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
tasks = cu.get_tasks_from_space(
|
tasks = cu.get_tasks_from_space(config.clickup.space_id, statuses=["to do"])
|
||||||
config.clickup.space_id, statuses=["to do"]
|
|
||||||
)
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
if task.task_type != "Link Building":
|
if task.task_type != "Link Building":
|
||||||
continue
|
continue
|
||||||
lb_method = task.custom_fields.get("LB Method", "")
|
lb_method = task.custom_fields.get("LB Method", "")
|
||||||
if lb_method and lb_method != "Cora Backlinks":
|
if lb_method and lb_method != "Cora Backlinks":
|
||||||
continue
|
continue
|
||||||
result["pending_cora_runs"].append({
|
result["pending_cora_runs"].append(
|
||||||
|
{
|
||||||
"keyword": task.custom_fields.get("Keyword", ""),
|
"keyword": task.custom_fields.get("Keyword", ""),
|
||||||
"url": task.custom_fields.get("IMSURL", ""),
|
"url": task.custom_fields.get("IMSURL", ""),
|
||||||
"client": task.custom_fields.get("Client", ""),
|
"client": task.custom_fields.get("Client", ""),
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"task_name": task.name,
|
"task_name": task.name,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
cu.close()
|
cu.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -323,9 +323,7 @@ class ClickUpClient:
|
||||||
log.info("Created custom field '%s' (%s) on list %s", name, field_type, list_id)
|
log.info("Created custom field '%s' (%s) on list %s", name, field_type, list_id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def discover_field_filter(
|
def discover_field_filter(self, list_id: str, field_name: str) -> dict[str, Any] | None:
|
||||||
self, list_id: str, field_name: str
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Discover a custom field's UUID and dropdown option map.
|
"""Discover a custom field's UUID and dropdown option map.
|
||||||
|
|
||||||
Returns {"field_id": "<uuid>", "options": {"Press Release": "<opt_id>", ...}}
|
Returns {"field_id": "<uuid>", "options": {"Press Release": "<opt_id>", ...}}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
@ -257,9 +258,7 @@ class Scheduler:
|
||||||
field_id = self._field_filter_cache["field_id"]
|
field_id = self._field_filter_cache["field_id"]
|
||||||
options = self._field_filter_cache["options"]
|
options = self._field_filter_cache["options"]
|
||||||
# Only include options that map to skills we have
|
# Only include options that map to skills we have
|
||||||
matching_opt_ids = [
|
matching_opt_ids = [options[name] for name in skill_map if name in options]
|
||||||
options[name] for name in skill_map if name in options
|
|
||||||
]
|
|
||||||
if matching_opt_ids:
|
if matching_opt_ids:
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
|
|
@ -335,9 +334,7 @@ class Scheduler:
|
||||||
self.db.kv_set(kv_key, json.dumps(state))
|
self.db.kv_set(kv_key, json.dumps(state))
|
||||||
|
|
||||||
log.info("Executing ClickUp task: %s → %s", task.name, tool_name)
|
log.info("Executing ClickUp task: %s → %s", task.name, tool_name)
|
||||||
self._notify(
|
self._notify(f"Executing ClickUp task: **{task.name}** → Skill: `{tool_name}`")
|
||||||
f"Executing ClickUp task: **{task.name}** → Skill: `{tool_name}`"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Build tool arguments from field mapping
|
# Build tool arguments from field mapping
|
||||||
|
|
@ -376,9 +373,7 @@ class Scheduler:
|
||||||
self.db.kv_set(kv_key, json.dumps(state))
|
self.db.kv_set(kv_key, json.dumps(state))
|
||||||
|
|
||||||
client.update_task_status(task_id, self.config.clickup.review_status)
|
client.update_task_status(task_id, self.config.clickup.review_status)
|
||||||
attach_note = (
|
attach_note = f"\n📎 {uploaded_count} file(s) attached." if uploaded_count else ""
|
||||||
f"\n📎 {uploaded_count} file(s) attached." if uploaded_count else ""
|
|
||||||
)
|
|
||||||
comment = (
|
comment = (
|
||||||
f"✅ CheddahBot completed this task.\n\n"
|
f"✅ CheddahBot completed this task.\n\n"
|
||||||
f"Skill: {tool_name}\n"
|
f"Skill: {tool_name}\n"
|
||||||
|
|
@ -478,12 +473,16 @@ class Scheduler:
|
||||||
def _process_watched_file(self, xlsx_path: Path, kv_key: str):
|
def _process_watched_file(self, xlsx_path: Path, kv_key: str):
|
||||||
"""Try to match a watched .xlsx file to a ClickUp task and run the pipeline."""
|
"""Try to match a watched .xlsx file to a ClickUp task and run the pipeline."""
|
||||||
filename = xlsx_path.name
|
filename = xlsx_path.name
|
||||||
# Normalize filename stem for matching (e.g., "precision-cnc-machining" → "precision cnc machining")
|
# Normalize filename stem for matching
|
||||||
|
# e.g., "precision-cnc-machining" → "precision cnc machining"
|
||||||
stem = xlsx_path.stem.lower().replace("-", " ").replace("_", " ")
|
stem = xlsx_path.stem.lower().replace("-", " ").replace("_", " ")
|
||||||
stem = re.sub(r"\s+", " ", stem).strip()
|
stem = re.sub(r"\s+", " ", stem).strip()
|
||||||
|
|
||||||
# Mark as processing
|
# Mark as processing
|
||||||
self.db.kv_set(kv_key, json.dumps({"status": "processing", "started_at": datetime.now(UTC).isoformat()}))
|
self.db.kv_set(
|
||||||
|
kv_key,
|
||||||
|
json.dumps({"status": "processing", "started_at": datetime.now(UTC).isoformat()}),
|
||||||
|
)
|
||||||
|
|
||||||
# Try to find matching ClickUp task
|
# Try to find matching ClickUp task
|
||||||
matched_task = None
|
matched_task = None
|
||||||
|
|
@ -492,15 +491,21 @@ class Scheduler:
|
||||||
|
|
||||||
if not matched_task:
|
if not matched_task:
|
||||||
log.warning("No ClickUp task match for '%s' — skipping", filename)
|
log.warning("No ClickUp task match for '%s' — skipping", filename)
|
||||||
self.db.kv_set(kv_key, json.dumps({
|
self.db.kv_set(
|
||||||
|
kv_key,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
"status": "unmatched",
|
"status": "unmatched",
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"stem": stem,
|
"stem": stem,
|
||||||
"checked_at": datetime.now(UTC).isoformat(),
|
"checked_at": datetime.now(UTC).isoformat(),
|
||||||
}))
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
self._notify(
|
self._notify(
|
||||||
f"Folder watcher: no ClickUp match for **{filename}**.\n"
|
f"Folder watcher: no ClickUp match for **{filename}**.\n"
|
||||||
f"Create a Link Building task with Keyword matching '{stem}' to enable auto-processing.",
|
f"Create a Link Building task with Keyword "
|
||||||
|
f"matching '{stem}' to enable auto-processing.",
|
||||||
category="linkbuilding",
|
category="linkbuilding",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -527,10 +532,8 @@ class Scheduler:
|
||||||
# Parse branded_plus_ratio
|
# Parse branded_plus_ratio
|
||||||
bp_raw = matched_task.custom_fields.get("BrandedPlusRatio", "")
|
bp_raw = matched_task.custom_fields.get("BrandedPlusRatio", "")
|
||||||
if bp_raw:
|
if bp_raw:
|
||||||
try:
|
with contextlib.suppress(ValueError, TypeError):
|
||||||
args["branded_plus_ratio"] = float(bp_raw)
|
args["branded_plus_ratio"] = float(bp_raw)
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute via tool registry
|
# Execute via tool registry
|
||||||
|
|
@ -541,13 +544,18 @@ class Scheduler:
|
||||||
|
|
||||||
if "Error" in result and "## Step" not in result:
|
if "Error" in result and "## Step" not in result:
|
||||||
# Pipeline failed
|
# Pipeline failed
|
||||||
self.db.kv_set(kv_key, json.dumps({
|
self.db.kv_set(
|
||||||
|
kv_key,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"error": result[:500],
|
"error": result[:500],
|
||||||
"failed_at": datetime.now(UTC).isoformat(),
|
"failed_at": datetime.now(UTC).isoformat(),
|
||||||
}))
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
self._notify(
|
self._notify(
|
||||||
f"Folder watcher: pipeline **failed** for **{filename}**.\n"
|
f"Folder watcher: pipeline **failed** for **{filename}**.\n"
|
||||||
f"Error: {result[:200]}",
|
f"Error: {result[:200]}",
|
||||||
|
|
@ -564,12 +572,17 @@ class Scheduler:
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.warning("Could not move %s to processed: %s", filename, e)
|
log.warning("Could not move %s to processed: %s", filename, e)
|
||||||
|
|
||||||
self.db.kv_set(kv_key, json.dumps({
|
self.db.kv_set(
|
||||||
|
kv_key,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"completed_at": datetime.now(UTC).isoformat(),
|
"completed_at": datetime.now(UTC).isoformat(),
|
||||||
}))
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
self._notify(
|
self._notify(
|
||||||
f"Folder watcher: pipeline **completed** for **{filename}**.\n"
|
f"Folder watcher: pipeline **completed** for **{filename}**.\n"
|
||||||
f"ClickUp task: {matched_task.name}",
|
f"ClickUp task: {matched_task.name}",
|
||||||
|
|
@ -578,13 +591,18 @@ class Scheduler:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error("Folder watcher pipeline error for %s: %s", filename, e)
|
log.error("Folder watcher pipeline error for %s: %s", filename, e)
|
||||||
self.db.kv_set(kv_key, json.dumps({
|
self.db.kv_set(
|
||||||
|
kv_key,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"error": str(e)[:500],
|
"error": str(e)[:500],
|
||||||
"failed_at": datetime.now(UTC).isoformat(),
|
"failed_at": datetime.now(UTC).isoformat(),
|
||||||
}))
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def _match_xlsx_to_clickup(self, normalized_stem: str):
|
def _match_xlsx_to_clickup(self, normalized_stem: str):
|
||||||
"""Find a ClickUp Link Building task whose Keyword matches the file stem.
|
"""Find a ClickUp Link Building task whose Keyword matches the file stem.
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,14 @@ def _get_blm_dir(ctx: dict | None) -> str:
|
||||||
return os.getenv("BLM_DIR", "E:/dev/Big-Link-Man")
|
return os.getenv("BLM_DIR", "E:/dev/Big-Link-Man")
|
||||||
|
|
||||||
|
|
||||||
def _run_blm_command(args: list[str], blm_dir: str, timeout: int = 1800) -> subprocess.CompletedProcess:
|
def _run_blm_command(
|
||||||
|
args: list[str], blm_dir: str, timeout: int = 1800
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
"""Run a Big-Link-Man CLI command via subprocess.
|
"""Run a Big-Link-Man CLI command via subprocess.
|
||||||
|
|
||||||
Always injects -u/-p from BLM_USERNAME/BLM_PASSWORD env vars.
|
Always injects -u/-p from BLM_USERNAME/BLM_PASSWORD env vars.
|
||||||
"""
|
"""
|
||||||
cmd = ["uv", "run", "python", "main.py"] + args
|
cmd = ["uv", "run", "python", "main.py", *args]
|
||||||
|
|
||||||
# Inject credentials from env vars
|
# Inject credentials from env vars
|
||||||
username = os.getenv("BLM_USERNAME", "")
|
username = os.getenv("BLM_USERNAME", "")
|
||||||
|
|
@ -521,7 +523,7 @@ def run_cora_backlinks(
|
||||||
project_id = ingest_parsed["project_id"]
|
project_id = ingest_parsed["project_id"]
|
||||||
job_file = ingest_parsed["job_file"]
|
job_file = ingest_parsed["job_file"]
|
||||||
|
|
||||||
output_parts.append(f"## Step 1: Ingest CORA Report")
|
output_parts.append("## Step 1: Ingest CORA Report")
|
||||||
output_parts.append(f"- Project: {project_name} (ID: {project_id})")
|
output_parts.append(f"- Project: {project_name} (ID: {project_id})")
|
||||||
output_parts.append(f"- Keyword: {ingest_parsed['main_keyword']}")
|
output_parts.append(f"- Keyword: {ingest_parsed['main_keyword']}")
|
||||||
output_parts.append(f"- Job file: {job_file}")
|
output_parts.append(f"- Job file: {job_file}")
|
||||||
|
|
@ -529,7 +531,9 @@ def run_cora_backlinks(
|
||||||
|
|
||||||
if clickup_task_id:
|
if clickup_task_id:
|
||||||
_sync_clickup(
|
_sync_clickup(
|
||||||
ctx, clickup_task_id, "ingest_done",
|
ctx,
|
||||||
|
clickup_task_id,
|
||||||
|
"ingest_done",
|
||||||
f"✅ CORA report ingested. Project ID: {project_id}. Job file: {job_file}",
|
f"✅ CORA report ingested. Project ID: {project_id}. Job file: {job_file}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -563,7 +567,7 @@ def run_cora_backlinks(
|
||||||
_fail_clickup_task(ctx, clickup_task_id, error)
|
_fail_clickup_task(ctx, clickup_task_id, error)
|
||||||
return "\n".join(output_parts) + f"\n\nError: {error}"
|
return "\n".join(output_parts) + f"\n\nError: {error}"
|
||||||
|
|
||||||
output_parts.append(f"## Step 2: Generate Content Batch")
|
output_parts.append("## Step 2: Generate Content Batch")
|
||||||
output_parts.append(f"- Status: {'Success' if gen_parsed['success'] else 'Completed'}")
|
output_parts.append(f"- Status: {'Success' if gen_parsed['success'] else 'Completed'}")
|
||||||
if gen_parsed["job_moved_to"]:
|
if gen_parsed["job_moved_to"]:
|
||||||
output_parts.append(f"- Job moved to: {gen_parsed['job_moved_to']}")
|
output_parts.append(f"- Job moved to: {gen_parsed['job_moved_to']}")
|
||||||
|
|
@ -583,7 +587,7 @@ def run_cora_backlinks(
|
||||||
|
|
||||||
output_parts.append("## ClickUp Sync")
|
output_parts.append("## ClickUp Sync")
|
||||||
output_parts.append(f"- Task `{clickup_task_id}` completed")
|
output_parts.append(f"- Task `{clickup_task_id}` completed")
|
||||||
output_parts.append(f"- Status set to 'complete'")
|
output_parts.append("- Status set to 'complete'")
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
return "\n".join(output_parts)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,95 @@ def _set_status(ctx: dict | None, message: str) -> None:
|
||||||
ctx["db"].kv_set("pipeline:status", message)
|
ctx["db"].kv_set("pipeline:status", message)
|
||||||
|
|
||||||
|
|
||||||
|
def _fuzzy_company_match(name: str, candidate: str) -> bool:
|
||||||
|
"""Check if company_name fuzzy-matches a candidate string.
|
||||||
|
|
||||||
|
Tries exact match, then substring containment in both directions.
|
||||||
|
"""
|
||||||
|
if not name or not candidate:
|
||||||
|
return False
|
||||||
|
a, b = name.lower().strip(), candidate.lower().strip()
|
||||||
|
return a == b or a in b or b in a
|
||||||
|
|
||||||
|
|
||||||
|
def _find_clickup_task(ctx: dict, company_name: str) -> str:
|
||||||
|
"""Query ClickUp API for a matching press-release task.
|
||||||
|
|
||||||
|
Looks for "to do" tasks where Work Category == "Press Release" and
|
||||||
|
the Client custom field fuzzy-matches company_name.
|
||||||
|
|
||||||
|
If found: creates kv_store "executing" entry, moves to "in progress"
|
||||||
|
on ClickUp, and returns the task ID.
|
||||||
|
If not found: returns "" (tool runs without ClickUp sync).
|
||||||
|
"""
|
||||||
|
cu_client = _get_clickup_client(ctx)
|
||||||
|
if not cu_client:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
config = ctx.get("config")
|
||||||
|
if not config or not config.clickup.space_id:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
tasks = cu_client.get_tasks_from_space(
|
||||||
|
config.clickup.space_id,
|
||||||
|
statuses=["to do"],
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("ClickUp API query failed in _find_clickup_task: %s", e)
|
||||||
|
return ""
|
||||||
|
finally:
|
||||||
|
cu_client.close()
|
||||||
|
|
||||||
|
# Find a task with Work Category == "Press Release" and Client matching company_name
|
||||||
|
for task in tasks:
|
||||||
|
if task.task_type != "Press Release":
|
||||||
|
continue
|
||||||
|
|
||||||
|
client_field = task.custom_fields.get("Client", "")
|
||||||
|
if not (
|
||||||
|
_fuzzy_company_match(company_name, task.name)
|
||||||
|
or _fuzzy_company_match(company_name, client_field)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Found a match — create kv_store entry and move to "in progress"
|
||||||
|
task_id = task.id
|
||||||
|
now = datetime.now(UTC).isoformat()
|
||||||
|
state = {
|
||||||
|
"state": "executing",
|
||||||
|
"clickup_task_id": task_id,
|
||||||
|
"clickup_task_name": task.name,
|
||||||
|
"task_type": task.task_type,
|
||||||
|
"skill_name": "write_press_releases",
|
||||||
|
"discovered_at": now,
|
||||||
|
"started_at": now,
|
||||||
|
"completed_at": None,
|
||||||
|
"error": None,
|
||||||
|
"deliverable_paths": [],
|
||||||
|
"custom_fields": task.custom_fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
db = ctx.get("db")
|
||||||
|
if db:
|
||||||
|
db.kv_set(f"clickup:task:{task_id}:state", json.dumps(state))
|
||||||
|
|
||||||
|
# Move to "in progress" on ClickUp
|
||||||
|
cu_client2 = _get_clickup_client(ctx)
|
||||||
|
if cu_client2:
|
||||||
|
try:
|
||||||
|
cu_client2.update_task_status(task_id, config.clickup.in_progress_status)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Failed to update ClickUp status for %s: %s", task_id, e)
|
||||||
|
finally:
|
||||||
|
cu_client2.close()
|
||||||
|
|
||||||
|
log.info("Auto-matched ClickUp task %s for company '%s'", task_id, company_name)
|
||||||
|
return task_id
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _get_clickup_client(ctx: dict | None):
|
def _get_clickup_client(ctx: dict | None):
|
||||||
"""Create a ClickUpClient from tool context, or None if unavailable."""
|
"""Create a ClickUpClient from tool context, or None if unavailable."""
|
||||||
if not ctx or not ctx.get("config") or not ctx["config"].clickup.enabled:
|
if not ctx or not ctx.get("config") or not ctx["config"].clickup.enabled:
|
||||||
|
|
@ -414,6 +503,12 @@ def write_press_releases(
|
||||||
# clickup_task_id is injected via ctx by the ToolRegistry (never from LLM)
|
# clickup_task_id is injected via ctx by the ToolRegistry (never from LLM)
|
||||||
clickup_task_id = ctx.get("clickup_task_id", "")
|
clickup_task_id = ctx.get("clickup_task_id", "")
|
||||||
|
|
||||||
|
# Fallback: auto-lookup from ClickUp API when invoked from chat (no task ID in ctx)
|
||||||
|
if not clickup_task_id and ctx.get("config"):
|
||||||
|
clickup_task_id = _find_clickup_task(ctx, company_name)
|
||||||
|
if clickup_task_id:
|
||||||
|
log.info("Chat-invoked PR: auto-linked to ClickUp task %s", clickup_task_id)
|
||||||
|
|
||||||
# ── ClickUp: set "in progress" and post starting comment ────────────
|
# ── ClickUp: set "in progress" and post starting comment ────────────
|
||||||
cu_client = None
|
cu_client = None
|
||||||
if clickup_task_id:
|
if clickup_task_id:
|
||||||
|
|
@ -421,9 +516,7 @@ def write_press_releases(
|
||||||
if cu_client:
|
if cu_client:
|
||||||
try:
|
try:
|
||||||
config = ctx["config"]
|
config = ctx["config"]
|
||||||
cu_client.update_task_status(
|
cu_client.update_task_status(clickup_task_id, config.clickup.in_progress_status)
|
||||||
clickup_task_id, config.clickup.in_progress_status
|
|
||||||
)
|
|
||||||
cu_client.add_comment(
|
cu_client.add_comment(
|
||||||
clickup_task_id,
|
clickup_task_id,
|
||||||
f"🔄 CheddahBot starting press release creation.\n\n"
|
f"🔄 CheddahBot starting press release creation.\n\n"
|
||||||
|
|
@ -604,7 +697,9 @@ def write_press_releases(
|
||||||
f"{uploaded_count} file(s) attached.\n"
|
f"{uploaded_count} file(s) attached.\n"
|
||||||
f"Generating JSON-LD schemas next...",
|
f"Generating JSON-LD schemas next...",
|
||||||
)
|
)
|
||||||
log.info("ClickUp: uploaded %d attachments for task %s", uploaded_count, clickup_task_id)
|
log.info(
|
||||||
|
"ClickUp: uploaded %d attachments for task %s", uploaded_count, clickup_task_id
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("ClickUp attachment upload failed for %s: %s", clickup_task_id, e)
|
log.warning("ClickUp attachment upload failed for %s: %s", clickup_task_id, e)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -22,7 +21,6 @@ from cheddahbot.tools.linkbuilding import (
|
||||||
scan_cora_folder,
|
scan_cora_folder,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fixtures
|
# Fixtures
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -127,7 +125,11 @@ class TestParseIngestOutput:
|
||||||
assert result["job_file"] == ""
|
assert result["job_file"] == ""
|
||||||
|
|
||||||
def test_project_with_special_chars(self):
|
def test_project_with_special_chars(self):
|
||||||
stdout = "Success: Project 'O'Brien & Sons (LLC)' created (ID: 7)\nJob file created: jobs/obrien.json\n"
|
stdout = (
|
||||||
|
"Success: Project 'O'Brien & Sons (LLC)'"
|
||||||
|
" created (ID: 7)\n"
|
||||||
|
"Job file created: jobs/obrien.json\n"
|
||||||
|
)
|
||||||
result = _parse_ingest_output(stdout)
|
result = _parse_ingest_output(stdout)
|
||||||
# Regex won't match greedy quote - that's ok, just verify no crash
|
# Regex won't match greedy quote - that's ok, just verify no crash
|
||||||
assert result["job_file"] == "jobs/obrien.json"
|
assert result["job_file"] == "jobs/obrien.json"
|
||||||
|
|
@ -310,7 +312,9 @@ class TestRunCoraBacklinks:
|
||||||
assert "not found" in result
|
assert "not found" in result
|
||||||
|
|
||||||
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
||||||
def test_happy_path(self, mock_cmd, mock_ctx, tmp_path, ingest_success_stdout, generate_success_stdout):
|
def test_happy_path(
|
||||||
|
self, mock_cmd, mock_ctx, tmp_path, ingest_success_stdout, generate_success_stdout
|
||||||
|
):
|
||||||
xlsx = tmp_path / "test.xlsx"
|
xlsx = tmp_path / "test.xlsx"
|
||||||
xlsx.write_text("fake data")
|
xlsx.write_text("fake data")
|
||||||
|
|
||||||
|
|
@ -324,9 +328,7 @@ class TestRunCoraBacklinks:
|
||||||
)
|
)
|
||||||
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test Project", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test Project", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "Step 1: Ingest CORA Report" in result
|
assert "Step 1: Ingest CORA Report" in result
|
||||||
assert "Step 2: Generate Content Batch" in result
|
assert "Step 2: Generate Content Batch" in result
|
||||||
|
|
@ -342,9 +344,7 @@ class TestRunCoraBacklinks:
|
||||||
args=[], returncode=1, stdout="Error: parsing failed", stderr="traceback"
|
args=[], returncode=1, stdout="Error: parsing failed", stderr="traceback"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "Error" in result
|
assert "Error" in result
|
||||||
assert "ingest-cora failed" in result
|
assert "ingest-cora failed" in result
|
||||||
|
|
||||||
|
|
@ -361,9 +361,7 @@ class TestRunCoraBacklinks:
|
||||||
)
|
)
|
||||||
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "Step 1: Ingest CORA Report" in result # Step 1 succeeded
|
assert "Step 1: Ingest CORA Report" in result # Step 1 succeeded
|
||||||
assert "generate-batch failed" in result
|
assert "generate-batch failed" in result
|
||||||
|
|
||||||
|
|
@ -374,9 +372,7 @@ class TestRunCoraBacklinks:
|
||||||
|
|
||||||
mock_cmd.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=1800)
|
mock_cmd.side_effect = subprocess.TimeoutExpired(cmd="test", timeout=1800)
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "timed out" in result
|
assert "timed out" in result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -395,9 +391,7 @@ class TestBlmIngestCora:
|
||||||
assert "Error" in result
|
assert "Error" in result
|
||||||
|
|
||||||
def test_file_not_found(self, mock_ctx):
|
def test_file_not_found(self, mock_ctx):
|
||||||
result = blm_ingest_cora(
|
result = blm_ingest_cora(xlsx_path="/nonexistent.xlsx", project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path="/nonexistent.xlsx", project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "not found" in result
|
assert "not found" in result
|
||||||
|
|
||||||
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
||||||
|
|
@ -409,9 +403,7 @@ class TestBlmIngestCora:
|
||||||
args=[], returncode=0, stdout=ingest_success_stdout, stderr=""
|
args=[], returncode=0, stdout=ingest_success_stdout, stderr=""
|
||||||
)
|
)
|
||||||
|
|
||||||
result = blm_ingest_cora(
|
result = blm_ingest_cora(xlsx_path=str(xlsx), project_name="Test Project", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test Project", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "CORA ingest complete" in result
|
assert "CORA ingest complete" in result
|
||||||
assert "ID: 42" in result
|
assert "ID: 42" in result
|
||||||
assert "jobs/test-project.json" in result
|
assert "jobs/test-project.json" in result
|
||||||
|
|
@ -425,9 +417,7 @@ class TestBlmIngestCora:
|
||||||
args=[], returncode=1, stdout="Error: bad file", stderr=""
|
args=[], returncode=1, stdout="Error: bad file", stderr=""
|
||||||
)
|
)
|
||||||
|
|
||||||
result = blm_ingest_cora(
|
result = blm_ingest_cora(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "Error" in result
|
assert "Error" in result
|
||||||
assert "ingest-cora failed" in result
|
assert "ingest-cora failed" in result
|
||||||
|
|
||||||
|
|
@ -585,9 +575,7 @@ class TestClickUpStateMachine:
|
||||||
)
|
)
|
||||||
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
mock_cmd.side_effect = [ingest_proc, gen_proc]
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "ClickUp Sync" in result
|
assert "ClickUp Sync" in result
|
||||||
|
|
||||||
|
|
@ -598,9 +586,7 @@ class TestClickUpStateMachine:
|
||||||
|
|
||||||
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
@patch("cheddahbot.tools.linkbuilding._run_blm_command")
|
||||||
@patch("cheddahbot.tools.linkbuilding._get_clickup_client")
|
@patch("cheddahbot.tools.linkbuilding._get_clickup_client")
|
||||||
def test_pipeline_sets_failed_state(
|
def test_pipeline_sets_failed_state(self, mock_cu, mock_cmd, mock_ctx, tmp_path):
|
||||||
self, mock_cu, mock_cmd, mock_ctx, tmp_path
|
|
||||||
):
|
|
||||||
xlsx = tmp_path / "test.xlsx"
|
xlsx = tmp_path / "test.xlsx"
|
||||||
xlsx.write_text("fake")
|
xlsx.write_text("fake")
|
||||||
|
|
||||||
|
|
@ -622,9 +608,7 @@ class TestClickUpStateMachine:
|
||||||
args=[], returncode=1, stdout="Error", stderr="crash"
|
args=[], returncode=1, stdout="Error", stderr="crash"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = run_cora_backlinks(
|
result = run_cora_backlinks(xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx)
|
||||||
xlsx_path=str(xlsx), project_name="Test", ctx=mock_ctx
|
|
||||||
)
|
|
||||||
assert "Error" in result
|
assert "Error" in result
|
||||||
|
|
||||||
raw = mock_ctx["db"].kv_get("clickup:task:task_fail:state")
|
raw = mock_ctx["db"].kv_get("clickup:task:task_fail:state")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue