qodev-apollo-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apollo_cli/__init__.py +3 -0
- apollo_cli/__main__.py +5 -0
- apollo_cli/app.py +120 -0
- apollo_cli/commands/__init__.py +0 -0
- apollo_cli/commands/accounts.py +51 -0
- apollo_cli/commands/calls.py +54 -0
- apollo_cli/commands/contacts.py +147 -0
- apollo_cli/commands/deals.py +51 -0
- apollo_cli/commands/emails.py +37 -0
- apollo_cli/commands/enrich.py +34 -0
- apollo_cli/commands/install.py +59 -0
- apollo_cli/commands/jobs.py +36 -0
- apollo_cli/commands/news.py +35 -0
- apollo_cli/commands/notes.py +68 -0
- apollo_cli/commands/people.py +34 -0
- apollo_cli/commands/pipelines.py +106 -0
- apollo_cli/commands/tasks.py +79 -0
- apollo_cli/commands/usage.py +56 -0
- apollo_cli/context.py +35 -0
- apollo_cli/formatters/__init__.py +0 -0
- apollo_cli/formatters/accounts.py +62 -0
- apollo_cli/formatters/contacts.py +80 -0
- apollo_cli/formatters/deals.py +46 -0
- apollo_cli/formatters/generic.py +101 -0
- apollo_cli/help_reference.py +123 -0
- apollo_cli/output.py +150 -0
- apollo_cli/skills/SKILL.md +200 -0
- apollo_cli/skills/__init__.py +1 -0
- apollo_cli/skills/references/__init__.py +1 -0
- apollo_cli/skills/references/account-workflows.md +202 -0
- apollo_cli/skills/references/contact-workflows.md +110 -0
- apollo_cli/skills/references/deal-workflows.md +142 -0
- qodev_apollo_cli-0.1.0.dist-info/METADATA +224 -0
- qodev_apollo_cli-0.1.0.dist-info/RECORD +37 -0
- qodev_apollo_cli-0.1.0.dist-info/WHEEL +4 -0
- qodev_apollo_cli-0.1.0.dist-info/entry_points.txt +2 -0
- qodev_apollo_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Notes command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from apollo_cli.context import ctx
|
|
10
|
+
from apollo_cli.formatters.generic import list_table
|
|
11
|
+
from apollo_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
notes_app = App(name="notes", help="Notes management.")
|
|
14
|
+
|
|
15
|
+
NOTE_LIST_COLUMNS = [
|
|
16
|
+
("ID", "id"),
|
|
17
|
+
("Content", "content"),
|
|
18
|
+
("Contact ID", "contact_id"),
|
|
19
|
+
("Account ID", "account_id"),
|
|
20
|
+
("Created", "created_at"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@notes_app.command
|
|
25
|
+
async def search(
|
|
26
|
+
*,
|
|
27
|
+
contact_id: Annotated[str | None, Parameter(name="--contact-id", help="Filter by contact ID")] = None,
|
|
28
|
+
account_id: Annotated[str | None, Parameter(name="--account-id", help="Filter by account ID")] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Search notes by contact or account."""
|
|
31
|
+
filters: dict = {}
|
|
32
|
+
if contact_id:
|
|
33
|
+
filters["contact_ids"] = [contact_id]
|
|
34
|
+
if account_id:
|
|
35
|
+
filters["account_ids"] = [account_id]
|
|
36
|
+
|
|
37
|
+
async with ctx.client() as client:
|
|
38
|
+
result = await client.search_notes(page=ctx.page, limit=ctx.limit, **filters)
|
|
39
|
+
|
|
40
|
+
output_list(
|
|
41
|
+
items=result.items,
|
|
42
|
+
total=result.total,
|
|
43
|
+
page=result.page,
|
|
44
|
+
limit=ctx.limit,
|
|
45
|
+
ctx=ctx,
|
|
46
|
+
format_fn=lambda items, **kw: list_table(items, NOTE_LIST_COLUMNS, title="Notes", **kw),
|
|
47
|
+
resource_name="Notes",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@notes_app.command
|
|
52
|
+
async def create(
|
|
53
|
+
*,
|
|
54
|
+
content: Annotated[str, Parameter(name="--content", help="Note content")],
|
|
55
|
+
contact_ids: Annotated[str | None, Parameter(name="--contact-ids", help="Comma-separated contact IDs")] = None,
|
|
56
|
+
account_ids: Annotated[str | None, Parameter(name="--account-ids", help="Comma-separated account IDs")] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Create a new note."""
|
|
59
|
+
kwargs: dict = {}
|
|
60
|
+
if contact_ids:
|
|
61
|
+
kwargs["contact_ids"] = [cid.strip() for cid in contact_ids.split(",")]
|
|
62
|
+
if account_ids:
|
|
63
|
+
kwargs["account_ids"] = [aid.strip() for aid in account_ids.split(",")]
|
|
64
|
+
|
|
65
|
+
async with ctx.client() as client:
|
|
66
|
+
result = await client.create_note(content, **kwargs)
|
|
67
|
+
|
|
68
|
+
output(result, ctx=ctx)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""People database search command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from apollo_cli.context import ctx
|
|
10
|
+
from apollo_cli.output import output
|
|
11
|
+
|
|
12
|
+
people_app = App(name="people", help="People database search.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@people_app.command
|
|
16
|
+
async def search(
|
|
17
|
+
*,
|
|
18
|
+
keywords: Annotated[str | None, Parameter(name="--keywords", help="Search keywords")] = None,
|
|
19
|
+
titles: Annotated[str | None, Parameter(name="--titles", help="Comma-separated job titles")] = None,
|
|
20
|
+
locations: Annotated[str | None, Parameter(name="--locations", help="Comma-separated locations")] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Search Apollo's global people database."""
|
|
23
|
+
filters: dict = {}
|
|
24
|
+
if keywords:
|
|
25
|
+
filters["q_keywords"] = keywords
|
|
26
|
+
if titles:
|
|
27
|
+
filters["person_titles"] = [t.strip() for t in titles.split(",")]
|
|
28
|
+
if locations:
|
|
29
|
+
filters["person_locations"] = [loc.strip() for loc in locations.split(",")]
|
|
30
|
+
|
|
31
|
+
async with ctx.client() as client:
|
|
32
|
+
result = await client.search_people(**filters)
|
|
33
|
+
|
|
34
|
+
output(result, ctx=ctx)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Pipelines and stages command groups."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from apollo_cli.context import ctx
|
|
10
|
+
from apollo_cli.formatters.generic import detail_table, list_table
|
|
11
|
+
from apollo_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
pipelines_app = App(name="pipelines", help="Pipeline management.")
|
|
14
|
+
stages_app = App(name="stages", help="Stage management.")
|
|
15
|
+
|
|
16
|
+
PIPELINE_LIST_COLUMNS = [
|
|
17
|
+
("ID", "id"),
|
|
18
|
+
("Title", "title"),
|
|
19
|
+
("Default", "default_pipeline"),
|
|
20
|
+
("Order", "display_order"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
PIPELINE_DETAIL_FIELDS = [
|
|
24
|
+
("ID", "id"),
|
|
25
|
+
("Title", "title"),
|
|
26
|
+
("Default", "default_pipeline"),
|
|
27
|
+
("Display Order", "display_order"),
|
|
28
|
+
("Source", "source"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
STAGE_LIST_COLUMNS = [
|
|
32
|
+
("ID", "id"),
|
|
33
|
+
("Name", "name"),
|
|
34
|
+
("Pipeline ID", "opportunity_pipeline_id"),
|
|
35
|
+
("Order", "display_order"),
|
|
36
|
+
("Probability", "probability"),
|
|
37
|
+
("Type", "type"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pipelines_app.command(name="list")
|
|
42
|
+
async def list_pipelines() -> None:
|
|
43
|
+
"""List all pipelines."""
|
|
44
|
+
async with ctx.client() as client:
|
|
45
|
+
result = await client.list_pipelines(page=ctx.page, limit=ctx.limit)
|
|
46
|
+
|
|
47
|
+
output_list(
|
|
48
|
+
items=result.items,
|
|
49
|
+
total=result.total,
|
|
50
|
+
page=result.page,
|
|
51
|
+
limit=ctx.limit,
|
|
52
|
+
ctx=ctx,
|
|
53
|
+
format_fn=lambda items, **kw: list_table(items, PIPELINE_LIST_COLUMNS, title="Pipelines", **kw),
|
|
54
|
+
resource_name="Pipelines",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pipelines_app.command
|
|
59
|
+
async def get(
|
|
60
|
+
id: Annotated[str, Parameter(help="Pipeline ID")],
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Get pipeline details by ID."""
|
|
63
|
+
async with ctx.client() as client:
|
|
64
|
+
pipeline = await client.get_pipeline(id)
|
|
65
|
+
|
|
66
|
+
output(
|
|
67
|
+
pipeline,
|
|
68
|
+
ctx=ctx,
|
|
69
|
+
format_fn=lambda d: detail_table(d, PIPELINE_DETAIL_FIELDS, title=f"Pipeline: {d.title or d.id}"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pipelines_app.command(name="stages")
|
|
74
|
+
async def pipeline_stages(
|
|
75
|
+
pipeline_id: Annotated[str, Parameter(help="Pipeline ID")],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""List stages for a specific pipeline."""
|
|
78
|
+
async with ctx.client() as client:
|
|
79
|
+
result = await client.list_pipeline_stages(pipeline_id, page=ctx.page, limit=ctx.limit)
|
|
80
|
+
|
|
81
|
+
output_list(
|
|
82
|
+
items=result.items,
|
|
83
|
+
total=result.total,
|
|
84
|
+
page=result.page,
|
|
85
|
+
limit=ctx.limit,
|
|
86
|
+
ctx=ctx,
|
|
87
|
+
format_fn=lambda items, **kw: list_table(items, STAGE_LIST_COLUMNS, title="Pipeline Stages", **kw),
|
|
88
|
+
resource_name="Stages",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@stages_app.command(name="list")
|
|
93
|
+
async def list_all_stages() -> None:
|
|
94
|
+
"""List all stages across all pipelines."""
|
|
95
|
+
async with ctx.client() as client:
|
|
96
|
+
result = await client.list_all_stages()
|
|
97
|
+
|
|
98
|
+
output_list(
|
|
99
|
+
items=result.items,
|
|
100
|
+
total=result.total,
|
|
101
|
+
page=result.page,
|
|
102
|
+
limit=ctx.limit,
|
|
103
|
+
ctx=ctx,
|
|
104
|
+
format_fn=lambda items, **kw: list_table(items, STAGE_LIST_COLUMNS, title="All Stages", **kw),
|
|
105
|
+
resource_name="Stages",
|
|
106
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tasks command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from cyclopts import App, Parameter
|
|
8
|
+
|
|
9
|
+
from apollo_cli.context import ctx
|
|
10
|
+
from apollo_cli.formatters.generic import list_table
|
|
11
|
+
from apollo_cli.output import output, output_list
|
|
12
|
+
|
|
13
|
+
tasks_app = App(name="tasks", help="Task management.")
|
|
14
|
+
|
|
15
|
+
TASK_LIST_COLUMNS = [
|
|
16
|
+
("ID", "id"),
|
|
17
|
+
("Subject", "subject"),
|
|
18
|
+
("Type", "type"),
|
|
19
|
+
("Priority", "priority"),
|
|
20
|
+
("Status", "status"),
|
|
21
|
+
("Due", "due_at"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@tasks_app.command
|
|
26
|
+
async def search(
|
|
27
|
+
*,
|
|
28
|
+
type: Annotated[str | None, Parameter(name="--type", help="Filter by task type (call, action_item, etc.)")] = None,
|
|
29
|
+
contact_id: Annotated[str | None, Parameter(name="--contact-id", help="Filter by contact ID")] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Search tasks."""
|
|
32
|
+
filters: dict = {}
|
|
33
|
+
if type:
|
|
34
|
+
filters["task_type_cds"] = [type]
|
|
35
|
+
if contact_id:
|
|
36
|
+
filters["contact_ids"] = [contact_id]
|
|
37
|
+
|
|
38
|
+
async with ctx.client() as client:
|
|
39
|
+
result = await client.search_tasks(page=ctx.page, limit=ctx.limit, **filters)
|
|
40
|
+
|
|
41
|
+
output_list(
|
|
42
|
+
items=result.items,
|
|
43
|
+
total=result.total,
|
|
44
|
+
page=result.page,
|
|
45
|
+
limit=ctx.limit,
|
|
46
|
+
ctx=ctx,
|
|
47
|
+
format_fn=lambda items, **kw: list_table(items, TASK_LIST_COLUMNS, title="Tasks", **kw),
|
|
48
|
+
resource_name="Tasks",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@tasks_app.command
|
|
53
|
+
async def create(
|
|
54
|
+
*,
|
|
55
|
+
contact_ids: Annotated[str, Parameter(name="--contact-ids", help="Comma-separated contact IDs")],
|
|
56
|
+
note: Annotated[str, Parameter(name="--note", help="Task description")],
|
|
57
|
+
type: Annotated[str, Parameter(name="--type", help="Task type")] = "action_item",
|
|
58
|
+
priority: Annotated[str, Parameter(name="--priority", help="Priority (high, medium, low)")] = "medium",
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Create a new task."""
|
|
61
|
+
ids = [cid.strip() for cid in contact_ids.split(",")]
|
|
62
|
+
|
|
63
|
+
async with ctx.client() as client:
|
|
64
|
+
result = await client.create_task(contact_ids=ids, note=note, type=type, priority=priority)
|
|
65
|
+
|
|
66
|
+
output(result, ctx=ctx)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@tasks_app.command
|
|
70
|
+
async def complete(
|
|
71
|
+
id: Annotated[str, Parameter(help="Task ID")],
|
|
72
|
+
*,
|
|
73
|
+
note: Annotated[str | None, Parameter(name="--note", help="Completion note")] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Mark a task as completed."""
|
|
76
|
+
async with ctx.client() as client:
|
|
77
|
+
result = await client.complete_task(id, note=note)
|
|
78
|
+
|
|
79
|
+
output(result, ctx=ctx)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Usage stats command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from cyclopts import App
|
|
6
|
+
|
|
7
|
+
from apollo_cli.context import ctx
|
|
8
|
+
from apollo_cli.output import output_json, output_markdown
|
|
9
|
+
|
|
10
|
+
usage_app = App(name="usage", help="API usage statistics.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@usage_app.default
|
|
14
|
+
async def usage() -> None:
|
|
15
|
+
"""Show API usage statistics and rate limits."""
|
|
16
|
+
async with ctx.client() as client:
|
|
17
|
+
data = await client.get_api_usage()
|
|
18
|
+
rate = client.rate_limit_status
|
|
19
|
+
|
|
20
|
+
if ctx.json_mode:
|
|
21
|
+
output_json({"usage": data, "rate_limits": rate})
|
|
22
|
+
else:
|
|
23
|
+
md = _format_usage(data, rate)
|
|
24
|
+
output_markdown(md)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _format_usage(data: dict, rate: dict) -> str:
|
|
28
|
+
lines = ["# API Usage"]
|
|
29
|
+
|
|
30
|
+
if rate:
|
|
31
|
+
lines.append("")
|
|
32
|
+
lines.append("## Rate Limits")
|
|
33
|
+
lines.append("")
|
|
34
|
+
lines.append("| Window | Limit | Remaining |")
|
|
35
|
+
lines.append("|--------|-------|-----------|")
|
|
36
|
+
if "hourly_limit" in rate:
|
|
37
|
+
lines.append(f"| Hourly | {rate.get('hourly_limit', '?')} | {rate.get('hourly_left', '?')} |")
|
|
38
|
+
if "minute_limit" in rate:
|
|
39
|
+
lines.append(f"| Minute | {rate.get('minute_limit', '?')} | {rate.get('minute_left', '?')} |")
|
|
40
|
+
if "daily_limit" in rate:
|
|
41
|
+
lines.append(f"| Daily | {rate.get('daily_limit', '?')} | {rate.get('daily_left', '?')} |")
|
|
42
|
+
|
|
43
|
+
if data:
|
|
44
|
+
lines.append("")
|
|
45
|
+
lines.append("## Endpoint Usage")
|
|
46
|
+
lines.append("")
|
|
47
|
+
lines.append("| Endpoint | Day Consumed | Day Left | Hour Consumed | Hour Left |")
|
|
48
|
+
lines.append("|----------|-------------|----------|--------------|-----------|")
|
|
49
|
+
for endpoint, stats in data.items():
|
|
50
|
+
day_consumed = stats.get("day", {}).get("consumed", "?")
|
|
51
|
+
day_left = stats.get("day", {}).get("left_over", "?")
|
|
52
|
+
hour_consumed = stats.get("hour", {}).get("consumed", "?")
|
|
53
|
+
hour_left = stats.get("hour", {}).get("left_over", "?")
|
|
54
|
+
lines.append(f"| {endpoint} | {day_consumed} | {day_left} | {hour_consumed} | {hour_left} |")
|
|
55
|
+
|
|
56
|
+
return "\n".join(lines)
|
apollo_cli/context.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Global context shared across all commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from qodev_apollo_api import ApolloClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Context:
|
|
12
|
+
"""Shared state passed from the meta launcher to every command."""
|
|
13
|
+
|
|
14
|
+
json_mode: bool = False
|
|
15
|
+
api_key: str | None = None
|
|
16
|
+
limit: int = 25
|
|
17
|
+
page: int = 1
|
|
18
|
+
|
|
19
|
+
def client(self) -> ApolloClient:
|
|
20
|
+
"""Create a new ApolloClient with the configured API key."""
|
|
21
|
+
kwargs: dict = {}
|
|
22
|
+
if self.api_key:
|
|
23
|
+
kwargs["api_key"] = self.api_key
|
|
24
|
+
return ApolloClient(**kwargs)
|
|
25
|
+
|
|
26
|
+
def configure(self, *, json_mode: bool, api_key: str | None, limit: int, page: int) -> None:
|
|
27
|
+
"""Update context fields in place (preserves references held by command modules)."""
|
|
28
|
+
self.json_mode = json_mode
|
|
29
|
+
self.api_key = api_key
|
|
30
|
+
self.limit = limit
|
|
31
|
+
self.page = page
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Module-level singleton — updated in place by the meta launcher.
|
|
35
|
+
ctx = Context()
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Account-specific formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from apollo_cli.formatters.generic import detail_table, list_table
|
|
8
|
+
|
|
9
|
+
ACCOUNT_DETAIL_FIELDS = [
|
|
10
|
+
("ID", "id"),
|
|
11
|
+
("Name", "name"),
|
|
12
|
+
("Domain", "domain"),
|
|
13
|
+
("Phone", "phone"),
|
|
14
|
+
("Industry", "industry"),
|
|
15
|
+
("Employees", "estimated_num_employees"),
|
|
16
|
+
("Revenue", "annual_revenue_printed"),
|
|
17
|
+
("LinkedIn", "linkedin_url"),
|
|
18
|
+
("Website", "website_url"),
|
|
19
|
+
("City", "city"),
|
|
20
|
+
("State", "state"),
|
|
21
|
+
("Country", "country"),
|
|
22
|
+
("Stage ID", "account_stage_id"),
|
|
23
|
+
("Owner ID", "owner_id"),
|
|
24
|
+
("Contacts", "num_contacts"),
|
|
25
|
+
("Founded", "founded_year"),
|
|
26
|
+
("Description", "short_description"),
|
|
27
|
+
("Created", "created_at"),
|
|
28
|
+
("Last Activity", "last_activity_date"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
ACCOUNT_LIST_COLUMNS = [
|
|
32
|
+
("Name", "name"),
|
|
33
|
+
("Domain", "domain"),
|
|
34
|
+
("Industry", "industry"),
|
|
35
|
+
("Employees", "estimated_num_employees"),
|
|
36
|
+
("City", "city"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_account_detail(data: Any) -> str:
|
|
41
|
+
"""Format a single account as a markdown detail view."""
|
|
42
|
+
name = data.name if hasattr(data, "name") else data.get("name", "Unknown")
|
|
43
|
+
md = detail_table(data, ACCOUNT_DETAIL_FIELDS, title=f"Account: {name}")
|
|
44
|
+
|
|
45
|
+
# Technology stack (detail endpoint)
|
|
46
|
+
tech = getattr(data, "technology_names", None) or (data.get("technology_names") if isinstance(data, dict) else None)
|
|
47
|
+
if tech:
|
|
48
|
+
md += "\n\n## Technology Stack\n"
|
|
49
|
+
md += "\n" + ", ".join(tech)
|
|
50
|
+
|
|
51
|
+
# Keywords
|
|
52
|
+
keywords = getattr(data, "keywords", None) or (data.get("keywords") if isinstance(data, dict) else None)
|
|
53
|
+
if keywords:
|
|
54
|
+
md += "\n\n## Keywords\n"
|
|
55
|
+
md += "\n" + ", ".join(keywords)
|
|
56
|
+
|
|
57
|
+
return md
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_account_list(items: list[Any], *, total: int = 0, page: int = 1) -> str:
|
|
61
|
+
"""Format a list of accounts as a markdown table."""
|
|
62
|
+
return list_table(items, ACCOUNT_LIST_COLUMNS, title="Accounts", total=total, page=page)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Contact-specific formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from apollo_cli.formatters.generic import detail_table, list_table
|
|
8
|
+
|
|
9
|
+
CONTACT_DETAIL_FIELDS = [
|
|
10
|
+
("ID", "id"),
|
|
11
|
+
("Name", "name"),
|
|
12
|
+
("Email", "email"),
|
|
13
|
+
("Title", "title"),
|
|
14
|
+
("Company", "organization_name"),
|
|
15
|
+
("LinkedIn", "linkedin_url"),
|
|
16
|
+
("Phone", "sanitized_phone"),
|
|
17
|
+
("Stage ID", "contact_stage_id"),
|
|
18
|
+
("City", "city"),
|
|
19
|
+
("State", "state"),
|
|
20
|
+
("Country", "country"),
|
|
21
|
+
("Source", "source"),
|
|
22
|
+
("Owner ID", "owner_id"),
|
|
23
|
+
("Created", "created_at"),
|
|
24
|
+
("Updated", "updated_at"),
|
|
25
|
+
("Last Activity", "last_activity_date"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
CONTACT_LIST_COLUMNS = [
|
|
29
|
+
("Name", "name"),
|
|
30
|
+
("Title", "title"),
|
|
31
|
+
("Company", "organization_name"),
|
|
32
|
+
("Email", "email"),
|
|
33
|
+
("LinkedIn", "linkedin_url"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_contact_detail(data: Any) -> str:
|
|
38
|
+
"""Format a single contact as a markdown detail view."""
|
|
39
|
+
name = data.name if hasattr(data, "name") else data.get("name", "Unknown")
|
|
40
|
+
md = detail_table(data, CONTACT_DETAIL_FIELDS, title=f"Contact: {name}")
|
|
41
|
+
|
|
42
|
+
# Phone numbers section
|
|
43
|
+
phones = data.phone_numbers if hasattr(data, "phone_numbers") else data.get("phone_numbers", [])
|
|
44
|
+
if phones:
|
|
45
|
+
md += "\n\n## Phone Numbers\n"
|
|
46
|
+
for p in phones:
|
|
47
|
+
if hasattr(p, "sanitized_number"):
|
|
48
|
+
num = p.sanitized_number or p.number
|
|
49
|
+
ptype = p.type or "unknown"
|
|
50
|
+
else:
|
|
51
|
+
num = p.get("sanitized_number") or p.get("number", "")
|
|
52
|
+
ptype = p.get("type", "unknown")
|
|
53
|
+
md += f"\n- {num} ({ptype})"
|
|
54
|
+
|
|
55
|
+
# Employment history (detail endpoint only)
|
|
56
|
+
history = getattr(data, "employment_history", None)
|
|
57
|
+
if history:
|
|
58
|
+
md += "\n\n## Employment History\n"
|
|
59
|
+
for entry in history:
|
|
60
|
+
org = entry.organization_name or "Unknown"
|
|
61
|
+
title = entry.title or "Unknown"
|
|
62
|
+
current = " (current)" if entry.current else ""
|
|
63
|
+
md += f"\n- **{title}** at {org}{current}"
|
|
64
|
+
|
|
65
|
+
return md
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_contact_list(items: list[Any], *, total: int = 0, page: int = 1) -> str:
|
|
69
|
+
"""Format a list of contacts as a markdown table."""
|
|
70
|
+
return list_table(items, CONTACT_LIST_COLUMNS, title="Contacts", total=total, page=page)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_stages_list(stages: list[Any], *, total: int = 0, page: int = 1) -> str:
|
|
74
|
+
"""Format contact stages."""
|
|
75
|
+
columns = [
|
|
76
|
+
("ID", "id"),
|
|
77
|
+
("Name", "name"),
|
|
78
|
+
("Display Order", "display_order"),
|
|
79
|
+
]
|
|
80
|
+
return list_table(stages, columns, title="Contact Stages", total=total, page=page)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Deal-specific formatters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from apollo_cli.formatters.generic import detail_table, list_table
|
|
8
|
+
|
|
9
|
+
DEAL_DETAIL_FIELDS = [
|
|
10
|
+
("ID", "id"),
|
|
11
|
+
("Name", "name"),
|
|
12
|
+
("Amount", "amount"),
|
|
13
|
+
("Stage", "stage_name"),
|
|
14
|
+
("Pipeline ID", "opportunity_pipeline_id"),
|
|
15
|
+
("Probability", "probability"),
|
|
16
|
+
("Close Date", "closed_date"),
|
|
17
|
+
("Won", "is_won"),
|
|
18
|
+
("Closed", "is_closed"),
|
|
19
|
+
("Account ID", "account_id"),
|
|
20
|
+
("Owner ID", "owner_id"),
|
|
21
|
+
("Source", "source"),
|
|
22
|
+
("Next Step", "next_step"),
|
|
23
|
+
("Next Step Date", "next_step_date"),
|
|
24
|
+
("Forecast Category", "forecast_category"),
|
|
25
|
+
("Created", "created_at"),
|
|
26
|
+
("Last Activity", "last_activity_date"),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEAL_LIST_COLUMNS = [
|
|
30
|
+
("Name", "name"),
|
|
31
|
+
("Amount", "amount"),
|
|
32
|
+
("Stage", "stage_name"),
|
|
33
|
+
("Close Date", "closed_date"),
|
|
34
|
+
("Won", "is_won"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def format_deal_detail(data: Any) -> str:
|
|
39
|
+
"""Format a single deal as a markdown detail view."""
|
|
40
|
+
name = data.name if hasattr(data, "name") else data.get("name", "Unknown")
|
|
41
|
+
return detail_table(data, DEAL_DETAIL_FIELDS, title=f"Deal: {name}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_deal_list(items: list[Any], *, total: int = 0, page: int = 1) -> str:
|
|
45
|
+
"""Format a list of deals as a markdown table."""
|
|
46
|
+
return list_table(items, DEAL_LIST_COLUMNS, title="Deals", total=total, page=page)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Generic formatters for Pydantic models and dicts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from apollo_cli.output import md_table
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def detail_table(data: Any, fields: list[tuple[str, str]], *, title: str | None = None) -> str:
|
|
13
|
+
"""Render a single record as a Field/Value markdown table.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
data: Pydantic model or dict.
|
|
17
|
+
fields: List of (label, key) — key supports dot notation for nested access.
|
|
18
|
+
title: Optional heading above the table.
|
|
19
|
+
"""
|
|
20
|
+
d = data.model_dump() if isinstance(data, BaseModel) else data
|
|
21
|
+
|
|
22
|
+
lines: list[str] = []
|
|
23
|
+
if title:
|
|
24
|
+
lines.append(f"# {title}")
|
|
25
|
+
lines.append("")
|
|
26
|
+
|
|
27
|
+
lines.append("| Field | Value |")
|
|
28
|
+
lines.append("|-------|-------|")
|
|
29
|
+
for label, key in fields:
|
|
30
|
+
value = _get(d, key)
|
|
31
|
+
if value is None or value == "" or value == []:
|
|
32
|
+
continue
|
|
33
|
+
lines.append(f"| {label} | {_fmt(value)} |")
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_table(
|
|
38
|
+
items: list[Any],
|
|
39
|
+
columns: list[tuple[str, str]],
|
|
40
|
+
*,
|
|
41
|
+
title: str = "Results",
|
|
42
|
+
total: int = 0,
|
|
43
|
+
page: int = 1,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Render a list of records as a markdown table.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
items: List of Pydantic models or dicts.
|
|
49
|
+
columns: List of (header_label, key) tuples.
|
|
50
|
+
title: Heading text.
|
|
51
|
+
total: Total results count (for pagination header).
|
|
52
|
+
page: Current page number.
|
|
53
|
+
"""
|
|
54
|
+
if not items:
|
|
55
|
+
return f"# {title}\n\n_No results found._"
|
|
56
|
+
|
|
57
|
+
header = f"# {title}"
|
|
58
|
+
if total:
|
|
59
|
+
header += f" (page {page}, {total} total)"
|
|
60
|
+
|
|
61
|
+
rows: list[dict[str, str]] = []
|
|
62
|
+
for item in items:
|
|
63
|
+
d = item.model_dump() if isinstance(item, BaseModel) else item
|
|
64
|
+
row = {}
|
|
65
|
+
for _, key in columns:
|
|
66
|
+
row[key] = _fmt(_get(d, key))
|
|
67
|
+
rows.append(row)
|
|
68
|
+
|
|
69
|
+
return f"{header}\n\n{md_table(rows, columns)}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get(d: dict, key: str) -> Any:
|
|
78
|
+
"""Get a value from a dict, supporting dot notation."""
|
|
79
|
+
parts = key.split(".")
|
|
80
|
+
current: Any = d
|
|
81
|
+
for part in parts:
|
|
82
|
+
if isinstance(current, dict):
|
|
83
|
+
current = current.get(part)
|
|
84
|
+
else:
|
|
85
|
+
return None
|
|
86
|
+
if current is None:
|
|
87
|
+
return None
|
|
88
|
+
return current
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fmt(value: Any) -> str:
|
|
92
|
+
"""Format a value for display in a markdown table cell."""
|
|
93
|
+
if value is None:
|
|
94
|
+
return ""
|
|
95
|
+
if isinstance(value, bool):
|
|
96
|
+
return "Yes" if value else "No"
|
|
97
|
+
if isinstance(value, list):
|
|
98
|
+
if not value:
|
|
99
|
+
return ""
|
|
100
|
+
return ", ".join(str(v) for v in value)
|
|
101
|
+
return str(value)
|