qodev-apollo-cli 1.2.1__tar.gz → 1.4.0__tar.gz

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.
Files changed (55) hide show
  1. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/.gitignore +5 -0
  2. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/CHANGELOG.md +26 -0
  3. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/PKG-INFO +2 -2
  4. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/pyproject.toml +2 -2
  5. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/app.py +0 -2
  6. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/calls.py +2 -19
  7. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/deals.py +49 -3
  8. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/jobs.py +2 -2
  9. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/SKILL.md +30 -9
  10. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/references/deal-workflows.md +1 -1
  11. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_commands.py +54 -0
  12. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/uv.lock +2 -2
  13. qodev_apollo_cli-1.2.1/src/apollo_cli/commands/news.py +0 -35
  14. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/.github/workflows/ci.yml +0 -0
  15. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/.github/workflows/publish.yml +0 -0
  16. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/LICENSE +0 -0
  17. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/README.md +0 -0
  18. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/__init__.py +0 -0
  19. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/__main__.py +0 -0
  20. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/__init__.py +0 -0
  21. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/accounts.py +0 -0
  22. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/contacts.py +0 -0
  23. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/conversations.py +0 -0
  24. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/custom_fields.py +0 -0
  25. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/emails.py +0 -0
  26. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/enrich.py +0 -0
  27. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/install.py +0 -0
  28. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/notes.py +0 -0
  29. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/people.py +0 -0
  30. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/pipelines.py +0 -0
  31. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/tasks.py +0 -0
  32. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/commands/usage.py +0 -0
  33. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/context.py +0 -0
  34. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/__init__.py +0 -0
  35. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/accounts.py +0 -0
  36. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/contacts.py +0 -0
  37. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/conversations.py +0 -0
  38. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/deals.py +0 -0
  39. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/generic.py +0 -0
  40. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/formatters/people.py +0 -0
  41. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/help_reference.py +0 -0
  42. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/linkedin.py +0 -0
  43. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/output.py +0 -0
  44. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/__init__.py +0 -0
  45. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/references/__init__.py +0 -0
  46. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/references/account-workflows.md +0 -0
  47. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/skills/references/contact-workflows.md +0 -0
  48. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/src/apollo_cli/util.py +0 -0
  49. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/conftest.py +0 -0
  50. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_context.py +0 -0
  51. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_conversations_formatter.py +0 -0
  52. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_install.py +0 -0
  53. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_linkedin.py +0 -0
  54. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_output.py +0 -0
  55. {qodev_apollo_cli-1.2.1 → qodev_apollo_cli-1.4.0}/tests/test_util.py +0 -0
@@ -7,3 +7,8 @@ dist/
7
7
  .coverage
8
8
  .pytest_cache/
9
9
  .ruff_cache/
10
+ .mypy_cache/
11
+
12
+ # Local secrets — never commit
13
+ .env
14
+ .env.*
@@ -6,6 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.4.0] - 2026-07-10
10
+
11
+ ### Fixed
12
+
13
+ - `deals search --query` now searches the **deal name** (`q_opportunity_name`). Apollo silently ignores `q_keywords` on `/opportunities/search`, so name search never actually filtered before.
14
+ - `jobs list` renders the organization job-postings shape (`Title/City/State/URL/Posted`) returned by the fixed API.
15
+
16
+ ### Changed
17
+
18
+ - Require `qodev-apollo-api>=0.5.0` (people `api_search`, deal name param, repaired `list_*` endpoints, search-filter validation).
19
+ - **Skill** (`SKILL.md` + references): added a "Search filter formats" section (seniority enums, location formats, `"min,max"` employee ranges, dict ranges, deal name search, canonical `linkedin_url`); documented `deals create` and `people --keywords`.
20
+
21
+ ### Removed
22
+
23
+ - **Breaking:** `news list` and `calls list` commands — the underlying Apollo routes were removed (`list_account_news` / `list_contact_calls` are gone from the API). `calls search` remains.
24
+
25
+ ## [1.3.0] - 2026-07-09
26
+
27
+ ### Added
28
+
29
+ - `deals create` command — create a deal/opportunity from the CLI: `deals create --name "…" [--owner-id] [--account-id] [--amount] [--stage-id | --stage-name] [--closed-date]`. `--name` is the only required flag; `--stage-name` resolves to an ID via the stage list (same helper as `deals search`), and passing both `--stage-id` and `--stage-name` is a validation error. Requires a master Apollo API key (non-master keys get a 403).
30
+
31
+ ### Changed
32
+
33
+ - Require `qodev-apollo-api>=0.4.0` (adds `create_deal`, and ships the fixed `update_opportunity_roles` payload so `deals set-role` now works, plus `search_accounts` filter validation).
34
+
9
35
  ## [1.2.1] - 2026-07-08
10
36
 
11
37
  Follow-ups from code review of the v1.2.0 changes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qodev-apollo-cli
3
- Version: 1.2.1
3
+ Version: 1.4.0
4
4
  Summary: Agent-friendly CLI for the Apollo API
5
5
  Project-URL: Homepage, https://github.com/qodevai/apollo-cli
6
6
  Project-URL: Repository, https://github.com/qodevai/apollo-cli
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.11
21
21
  Requires-Dist: cyclopts>=3.0
22
- Requires-Dist: qodev-apollo-api>=0.3.1
22
+ Requires-Dist: qodev-apollo-api>=0.5.0
23
23
  Requires-Dist: rich>=13.0
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: mypy>=1.13.0; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qodev-apollo-cli"
3
- version = "1.2.1"
3
+ version = "1.4.0"
4
4
  description = "Agent-friendly CLI for the Apollo API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,7 +20,7 @@ classifiers = [
20
20
  dependencies = [
21
21
  "cyclopts>=3.0",
22
22
  "rich>=13.0",
23
- "qodev-apollo-api>=0.3.1",
23
+ "qodev-apollo-api>=0.5.0",
24
24
  ]
25
25
 
26
26
  [project.optional-dependencies]
@@ -33,7 +33,6 @@ from apollo_cli.commands.emails import emails_app # noqa: E402
33
33
  from apollo_cli.commands.enrich import enrich_app # noqa: E402
34
34
  from apollo_cli.commands.install import install_app # noqa: E402
35
35
  from apollo_cli.commands.jobs import jobs_app # noqa: E402
36
- from apollo_cli.commands.news import news_app # noqa: E402
37
36
  from apollo_cli.commands.notes import notes_app # noqa: E402
38
37
  from apollo_cli.commands.people import people_app # noqa: E402
39
38
  from apollo_cli.commands.pipelines import pipelines_app, stages_app # noqa: E402
@@ -55,7 +54,6 @@ _sub_apps = [
55
54
  conversations_app,
56
55
  custom_fields_app,
57
56
  emails_app,
58
- news_app,
59
57
  jobs_app,
60
58
  usage_app,
61
59
  install_app,
@@ -2,13 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Annotated
6
-
7
- from cyclopts import App, Parameter
5
+ from cyclopts import App
8
6
 
9
7
  from apollo_cli.context import ctx
10
8
  from apollo_cli.formatters.generic import list_table
11
- from apollo_cli.output import output_json, output_list, output_markdown
9
+ from apollo_cli.output import output_list
12
10
 
13
11
  calls_app = App(name="calls", help="Call activity.")
14
12
 
@@ -37,18 +35,3 @@ async def search() -> None:
37
35
  format_fn=lambda items, **kw: list_table(items, CALL_LIST_COLUMNS, title="Calls", **kw),
38
36
  resource_name="Calls",
39
37
  )
40
-
41
-
42
- @calls_app.command(name="list")
43
- async def list_contact_calls(
44
- contact_id: Annotated[str, Parameter(help="Contact ID")],
45
- ) -> None:
46
- """List calls for a specific contact."""
47
- async with ctx.client() as client:
48
- result = await client.list_contact_calls(contact_id)
49
-
50
- if ctx.json_mode:
51
- output_json(result)
52
- else:
53
- md = list_table(result, CALL_LIST_COLUMNS, title="Contact Calls", total=len(result), page=1)
54
- output_markdown(md)
@@ -19,7 +19,7 @@ deals_app = App(name="deals", help="Manage deals/opportunities.")
19
19
  @deals_app.command
20
20
  async def search(
21
21
  *,
22
- query: Annotated[str, Parameter(name=["--query", "-q"], help="Search keyword")] = "",
22
+ query: Annotated[str, Parameter(name=["--query", "-q"], help="Search by deal name")] = "",
23
23
  stage_id: Annotated[str | None, Parameter(name="--stage-id", help="Filter by deal stage ID")] = None,
24
24
  stage_name: Annotated[
25
25
  str | None,
@@ -28,10 +28,12 @@ async def search(
28
28
  ),
29
29
  ] = None,
30
30
  ) -> None:
31
- """Search deals by keyword or filter."""
31
+ """Search deals by name or filter."""
32
32
  filters: dict = {}
33
33
  if query:
34
- filters["q_keywords"] = query
34
+ # Deal name search is q_opportunity_name — Apollo silently ignores
35
+ # q_keywords on /opportunities/search (it returns every deal).
36
+ filters["q_opportunity_name"] = query
35
37
  stage_ids: list[str] = []
36
38
  if stage_id:
37
39
  stage_ids.append(stage_id)
@@ -66,6 +68,50 @@ async def get(
66
68
  output(deal, ctx=ctx, format_fn=format_deal_detail)
67
69
 
68
70
 
71
+ @deals_app.command
72
+ async def create(
73
+ *,
74
+ name: Annotated[str, Parameter(name="--name", help="Deal name (required)")],
75
+ owner_id: Annotated[str | None, Parameter(name="--owner-id", help="Deal owner (team member) ID")] = None,
76
+ account_id: Annotated[str | None, Parameter(name="--account-id", help="Target account/company ID")] = None,
77
+ amount: Annotated[float | None, Parameter(name="--amount", help="Deal value (no currency symbol)")] = None,
78
+ stage_id: Annotated[str | None, Parameter(name="--stage-id", help="Deal stage ID")] = None,
79
+ stage_name: Annotated[
80
+ str | None,
81
+ Parameter(name="--stage-name", help="Deal stage name (resolved to an ID; avoids a `pipelines stages` lookup)"),
82
+ ] = None,
83
+ closed_date: Annotated[str | None, Parameter(name="--closed-date", help="Expected close date (YYYY-MM-DD)")] = None,
84
+ ) -> None:
85
+ """Create a new deal/opportunity.
86
+
87
+ Requires a master Apollo API key (non-master keys get a 403). ``--name`` is the
88
+ only required field. Use ``--stage-name`` to set the stage by name instead of ID.
89
+ """
90
+ if stage_id and stage_name:
91
+ error("Pass either --stage-id or --stage-name, not both.", ctx=ctx, code="conflicting_args", exit_code=2)
92
+ return
93
+
94
+ fields: dict[str, Any] = {}
95
+ if owner_id:
96
+ fields["owner_id"] = owner_id
97
+ if account_id:
98
+ fields["account_id"] = account_id
99
+ if amount is not None:
100
+ fields["amount"] = amount
101
+ if closed_date:
102
+ fields["closed_date"] = closed_date
103
+
104
+ async with ctx.client() as client:
105
+ if stage_name:
106
+ all_stages = await client.list_all_stages()
107
+ stage_id = resolve_stage_id(stage_name, all_stages.items, kind="deal stage")
108
+ if stage_id:
109
+ fields["opportunity_stage_id"] = stage_id
110
+ deal = await client.create_deal(name, **fields)
111
+
112
+ output(deal, ctx=ctx, format_fn=format_deal_detail)
113
+
114
+
69
115
  ROLE_TYPE_COLUMNS = [("ID", "id"), ("Name", "name"), ("Display Order", "display_order")]
70
116
 
71
117
 
@@ -14,8 +14,8 @@ jobs_app = App(name="jobs", help="Job postings.")
14
14
 
15
15
  JOBS_LIST_COLUMNS = [
16
16
  ("Title", "title"),
17
- ("Location", "location"),
18
- ("Department", "department"),
17
+ ("City", "city"),
18
+ ("State", "state"),
19
19
  ("URL", "url"),
20
20
  ("Posted", "posted_at"),
21
21
  ]
@@ -47,8 +47,9 @@ Get your API key from [Apollo.io Settings → API](https://app.apollo.io/#/setti
47
47
 
48
48
  | Command | Description |
49
49
  |---------|-------------|
50
- | `deals search [--query Q] [--stage-id ID] [--stage-name NAME]` | Search opportunities/deals |
50
+ | `deals search [--query Q] [--stage-id ID] [--stage-name NAME]` | Search deals. `--query` matches the **deal name** (`q_opportunity_name`) |
51
51
  | `deals get ID` | Get deal details |
52
+ | `deals create --name N [--owner-id ID] [--account-id ID] [--amount N] [--stage-id ID \| --stage-name NAME] [--closed-date YYYY-MM-DD]` | Create a deal (needs a master API key) |
52
53
  | `deals role-types` | List opportunity contact role types |
53
54
  | `deals set-role ID --contact-id C [--role-type NAME_OR_ID] [--primary]` | Set/update a contact's role on a deal |
54
55
 
@@ -78,7 +79,7 @@ Get your API key from [Apollo.io Settings → API](https://app.apollo.io/#/setti
78
79
 
79
80
  | Command | Description |
80
81
  |---------|-------------|
81
- | `people search [--titles T] [--seniorities S] [--locations L] [--organization-domains D]` | Search people database (respects `--limit`/`--page`) |
82
+ | `people search [--keywords K] [--titles T] [--seniorities S] [--locations L] [--organization-domains D]` | Search people database (respects `--limit`/`--page`). Returns teaser data only — no email/linkedin without enrichment. See **Search filter formats** below |
82
83
 
83
84
  ### notes
84
85
 
@@ -122,17 +123,11 @@ Recorded meetings (Zoom/Teams/Meet) with transcript and AI summary — distinct
122
123
  |---------|-------------|
123
124
  | `emails search` | Search email activities |
124
125
 
125
- ### news
126
-
127
- | Command | Description |
128
- |---------|-------------|
129
- | `news search [--categories CATS]` | Search news |
130
-
131
126
  ### jobs
132
127
 
133
128
  | Command | Description |
134
129
  |---------|-------------|
135
- | `jobs search [--job-titles TITLES] [--company-domains DOMAINS]` | Search job postings |
130
+ | `jobs list ACCOUNT_ID` | List job postings for an account's linked organization |
136
131
 
137
132
  ### usage
138
133
 
@@ -140,6 +135,32 @@ Recorded meetings (Zoom/Teams/Meet) with transcript and AI summary — distinct
140
135
  |---------|-------------|
141
136
  | `usage` | Show API usage stats and rate limits |
142
137
 
138
+ ## Search filter formats
139
+
140
+ Apollo **silently ignores** a filter whose value is in the wrong format (it returns an
141
+ unfiltered/whole page that looks like a real match) — so getting the format right matters.
142
+ These are empirically verified (each measurably narrows the result count):
143
+
144
+ **`people search`**
145
+ | Filter | Format | Notes |
146
+ |--------|--------|-------|
147
+ | `--keywords` | free text | names / titles / keywords |
148
+ | `--titles` | comma list of titles | e.g. `--titles "CEO,VP Sales"` |
149
+ | `--seniorities` | **lowercase enums** | `owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern`. `"VP"` / `"vice president"` match **nothing** |
150
+ | `--locations` | country name, **2-letter code** (`US`), or `"City, State, Country"` | `US` ≡ `United States` |
151
+ | `--organization-domains` | comma list of domains | e.g. `acme.com,globex.com` |
152
+
153
+ Raw API (via `qodev_apollo_api`) also accepts, on people search:
154
+ `organization_num_employees_ranges` as **`"min,max"` comma** strings (`["1,10"]`, `["1000,5000"]` — a dash `"1-10"` is silently ignored); `revenue_range`/`organization_num_jobs_range` as `{"min":N,"max":N}`; `contact_email_status` as `verified|unverified|likely to engage|unavailable`; `currently_using_any_of_technology_uids` as a list of tech UIDs.
155
+
156
+ **`deals search`** — `--query` searches the **deal name** (`q_opportunity_name`). *(Plain keyword search is not supported by Apollo for deals.)*
157
+
158
+ **`contacts search`** — `--linkedin-url` must be a real LinkedIn profile URL; the CLI canonicalizes it to Apollo's stored form (`http://www.linkedin.com/in/<slug>`). `--query` is free-text keywords.
159
+
160
+ **`accounts search`** — `--query` is a company-name keyword (`q_organization_name`).
161
+
162
+ **Stage filters** (`--stage-id`/`--stage-name` on contacts/deals) take a stage the account actually has — list them with `contacts stages` / `pipelines stages`.
163
+
143
164
  ## Exit Codes
144
165
 
145
166
  | Code | Meaning |
@@ -36,7 +36,7 @@ qodev-apollo-cli --json pipelines stages <pipeline-id> | jq '.items[] | {name, i
36
36
  # Search all deals
37
37
  qodev-apollo-cli deals search
38
38
 
39
- # Search by keyword
39
+ # Search by deal name (--query matches the deal name, not free keywords)
40
40
  qodev-apollo-cli deals search --query "enterprise"
41
41
 
42
42
  # Filter by stage
@@ -502,6 +502,60 @@ class TestDealRoleCommands:
502
502
  assert all("opportunity_contact_role_type_id" not in r for r in roles)
503
503
 
504
504
 
505
+ class TestDealCreate:
506
+ @pytest.mark.asyncio
507
+ async def test_create_passes_name_and_optional_fields(self, sample_deal: dict, capsys) -> None:
508
+ """deals create forwards --name plus only the optional fields that were provided."""
509
+ mock_client = MagicMock()
510
+ mock_client.create_deal = AsyncMock(return_value=sample_deal)
511
+
512
+ _ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
513
+
514
+ with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
515
+ from apollo_cli.commands.deals import create
516
+
517
+ await create(name="Big One", owner_id="o1", amount=5000, stage_id="st1")
518
+
519
+ name_arg = mock_client.create_deal.call_args.args[0]
520
+ fields = mock_client.create_deal.call_args.kwargs
521
+ assert name_arg == "Big One"
522
+ assert fields == {"owner_id": "o1", "amount": 5000, "opportunity_stage_id": "st1"}
523
+
524
+ @pytest.mark.asyncio
525
+ async def test_create_resolves_stage_name(self, sample_deal: dict, capsys) -> None:
526
+ """deals create --stage-name resolves to opportunity_stage_id."""
527
+ mock_client = MagicMock()
528
+ mock_client.list_all_stages = AsyncMock(
529
+ return_value=MockSearchResult(items=[{"id": "st-neg", "name": "Negotiation"}], total=1, page=1)
530
+ )
531
+ mock_client.create_deal = AsyncMock(return_value=sample_deal)
532
+
533
+ _ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
534
+
535
+ with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
536
+ from apollo_cli.commands.deals import create
537
+
538
+ await create(name="Deal", stage_name="negotiation")
539
+
540
+ assert mock_client.create_deal.call_args.kwargs["opportunity_stage_id"] == "st-neg"
541
+
542
+ @pytest.mark.asyncio
543
+ async def test_create_rejects_both_stage_id_and_name(self, capsys) -> None:
544
+ """Passing both --stage-id and --stage-name is a validation error; no API call is made."""
545
+ mock_client = MagicMock()
546
+ mock_client.create_deal = AsyncMock()
547
+
548
+ _ctx.ctx.configure(json_mode=True, api_key="test-key", limit=25, page=1)
549
+
550
+ with patch.object(_ctx.ctx, "client", return_value=MockAsyncContextManager(mock_client)):
551
+ from apollo_cli.commands.deals import create
552
+
553
+ with pytest.raises(SystemExit):
554
+ await create(name="Deal", stage_id="st1", stage_name="Negotiation")
555
+
556
+ mock_client.create_deal.assert_not_called()
557
+
558
+
505
559
  class TestCustomFieldsCommand:
506
560
  @pytest.mark.asyncio
507
561
  async def test_custom_fields_list_filters_by_modality(self, capsys) -> None:
@@ -472,7 +472,7 @@ wheels = [
472
472
 
473
473
  [[package]]
474
474
  name = "qodev-apollo-api"
475
- version = "0.3.2"
475
+ version = "0.5.0"
476
476
  source = { directory = "../apollo-api" }
477
477
  dependencies = [
478
478
  { name = "httpx" },
@@ -494,7 +494,7 @@ provides-extras = ["dev"]
494
494
 
495
495
  [[package]]
496
496
  name = "qodev-apollo-cli"
497
- version = "1.2.1"
497
+ version = "1.4.0"
498
498
  source = { editable = "." }
499
499
  dependencies = [
500
500
  { name = "cyclopts" },
@@ -1,35 +0,0 @@
1
- """News 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_json, output_markdown
12
-
13
- news_app = App(name="news", help="Account news.")
14
-
15
- NEWS_LIST_COLUMNS = [
16
- ("Title", "title"),
17
- ("Category", "category"),
18
- ("Published", "published_at"),
19
- ("URL", "url"),
20
- ]
21
-
22
-
23
- @news_app.command(name="list")
24
- async def list_news(
25
- account_id: Annotated[str, Parameter(help="Account ID")],
26
- ) -> None:
27
- """List news articles for an account."""
28
- async with ctx.client() as client:
29
- result = await client.list_account_news(account_id)
30
-
31
- if ctx.json_mode:
32
- output_json(result)
33
- else:
34
- md = list_table(result, NEWS_LIST_COLUMNS, title="Account News", total=len(result), page=1)
35
- output_markdown(md)