qodev-apollo-api 0.3.0__tar.gz → 0.3.2__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 (26) hide show
  1. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/CHANGELOG.md +12 -0
  2. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/PKG-INFO +1 -1
  3. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/pyproject.toml +1 -1
  4. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/__init__.py +2 -0
  5. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/client.py +10 -2
  6. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/models.py +13 -1
  7. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/test_client.py +23 -0
  8. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/test_models.py +3 -0
  9. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/uv.lock +1 -1
  10. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/.github/workflows/ci.yml +0 -0
  11. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/.github/workflows/publish.yml +0 -0
  12. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/.gitignore +0 -0
  13. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/.pre-commit-config.yaml +0 -0
  14. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/CLAUDE.md +0 -0
  15. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/LICENSE +0 -0
  16. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/Makefile +0 -0
  17. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/README.md +0 -0
  18. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/exceptions.py +0 -0
  19. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/py.typed +0 -0
  20. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/src/qodev_apollo_api/utils.py +0 -0
  21. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/__init__.py +0 -0
  22. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/integration/__init__.py +0 -0
  23. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/integration/validate_all_models.py +0 -0
  24. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/integration/validate_email_task_flow.py +0 -0
  25. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/test_exceptions.py +0 -0
  26. {qodev_apollo_api-0.3.0 → qodev_apollo_api-0.3.2}/tests/test_utils.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.2] - 2026-07-08
11
+
12
+ ### Fixed
13
+
14
+ - `search_tasks()` could raise `AttributeError` while *handling* an unparseable row: the skip path called `raw.get("id")` assuming `raw` was a dict, so a non-dict row (e.g. a stray `null`) turned the intended "skip one bad row" into a whole-page crash. The id lookup is now guarded (`isinstance(raw, dict)`), and iteration tolerates a null `tasks` value (`result.get("tasks") or []`).
15
+
16
+ ## [0.3.1] - 2026-07-08
17
+
18
+ ### Changed
19
+
20
+ - `update_opportunity_roles(...)` now types its `roles` parameter as `list[RoleAssignment]` (a `TypedDict` with a required `contact_id` and optional `opportunity_contact_role_type_id` / `is_primary`) instead of the loose `list[dict]`, giving callers type checking and autocomplete. `RoleAssignment` is exported from the package root. Non-breaking — plain dicts still satisfy it structurally.
21
+
10
22
  ## [0.3.0] - 2026-07-08
11
23
 
12
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qodev-apollo-api
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Async Python client for Apollo.io CRM API
5
5
  Project-URL: Homepage, https://github.com/qodevai/apollo-api
6
6
  Project-URL: Repository, https://github.com/qodevai/apollo-api
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qodev-apollo-api"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Async Python client for Apollo.io CRM API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -63,6 +63,7 @@ from .models import (
63
63
  PaginatedResponse,
64
64
  PhoneEntry,
65
65
  Pipeline,
66
+ RoleAssignment,
66
67
  SortOrder,
67
68
  Stage,
68
69
  Task,
@@ -141,6 +142,7 @@ __all__ = [
141
142
  "PhoneEntry",
142
143
  "Pipeline",
143
144
  "RateLimitError",
145
+ "RoleAssignment",
144
146
  "SortOrder",
145
147
  "Stage",
146
148
  "Task",
@@ -33,6 +33,7 @@ from .models import (
33
33
  OpportunityContactRoleType,
34
34
  PaginatedResponse,
35
35
  Pipeline,
36
+ RoleAssignment,
36
37
  SortOrder,
37
38
  Stage,
38
39
  Task,
@@ -521,7 +522,9 @@ class ApolloClient:
521
522
  page=1,
522
523
  )
523
524
 
524
- async def update_opportunity_roles(self, opportunity_id: str, roles: list[dict]) -> Deal:
525
+ async def update_opportunity_roles(
526
+ self, opportunity_id: str, roles: list[RoleAssignment]
527
+ ) -> Deal:
525
528
  """Set the contact roles on a deal/opportunity (undocumented endpoint).
526
529
 
527
530
  This **replaces** the full set of contact roles on the opportunity, so
@@ -756,7 +759,12 @@ class ApolloClient:
756
759
  result = await self._post("/tasks/search", data)
757
760
 
758
761
  tasks: list[Task] = []
759
- for raw in result.get("tasks", []):
762
+ for raw in result.get("tasks") or []:
763
+ if not isinstance(raw, dict):
764
+ # A non-dict row (e.g. a stray null) would make resolve_task raise
765
+ # AttributeError, not ValidationError — skip it before we get there.
766
+ logger.warning("Skipping non-dict task row: %r", raw)
767
+ continue
760
768
  try:
761
769
  tasks.append(resolve_task(raw))
762
770
  except ValidationError:
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from datetime import date, datetime
10
10
  from decimal import Decimal
11
11
  from enum import StrEnum
12
- from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
12
+ from typing import Annotated, Any, Generic, Literal, NotRequired, TypeAlias, TypedDict, TypeVar
13
13
 
14
14
  from pydantic import BaseModel, ConfigDict, Discriminator, Field, TypeAdapter, ValidationError
15
15
 
@@ -183,6 +183,18 @@ class OpportunityContactRole(ApolloModel):
183
183
  updated_at: datetime | None = None
184
184
 
185
185
 
186
+ class RoleAssignment(TypedDict):
187
+ """A single contact-role entry accepted by ``update_opportunity_roles``.
188
+
189
+ ``contact_id`` is required; ``opportunity_contact_role_type_id`` and
190
+ ``is_primary`` are optional.
191
+ """
192
+
193
+ contact_id: str
194
+ opportunity_contact_role_type_id: NotRequired[str | None]
195
+ is_primary: NotRequired[bool]
196
+
197
+
186
198
  class OpportunityContactRoleType(ApolloModel):
187
199
  """Role type definition for opportunity contact roles (e.g., Decision Maker, Buyer)."""
188
200
 
@@ -338,6 +338,29 @@ async def test_search_tasks_skips_unparseable_row(client: ApolloClient):
338
338
  assert [t.id for t in result.items] == ["t2"]
339
339
 
340
340
 
341
+ async def test_search_tasks_skips_non_dict_row(client: ApolloClient):
342
+ """A non-dict row (e.g. a stray null) is skipped without raising AttributeError."""
343
+ client._client.request.return_value = _make_response(
344
+ {
345
+ "tasks": [None, "oops", {"id": "t2", "type": "call"}],
346
+ "pagination": {"total_entries": 3},
347
+ }
348
+ )
349
+
350
+ result = await client.search_tasks()
351
+
352
+ assert [t.id for t in result.items] == ["t2"]
353
+
354
+
355
+ async def test_search_tasks_null_tasks_key(client: ApolloClient):
356
+ """A null `tasks` value doesn't crash iteration."""
357
+ client._client.request.return_value = _make_response({"tasks": None, "pagination": {}})
358
+
359
+ result = await client.search_tasks()
360
+
361
+ assert result.items == []
362
+
363
+
341
364
  async def test_search_tasks_with_sort(client: ApolloClient):
342
365
  """Test search_tasks with sort parameter generates multi_sort payload."""
343
366
  client._client.request.return_value = _make_response(
@@ -899,6 +899,9 @@ def test_resolve_task_preserves_unmodelled_status():
899
899
  assert result.status == "skipped"
900
900
  # comparison against the known-value enum still works via StrEnum equality
901
901
  assert result.status != TaskStatus.SCHEDULED
902
+ # ...and a modelled status still compares equal to its enum member
903
+ scheduled = resolve_task({"id": "2", "type": "linkedin_step_connect", "status": "scheduled"})
904
+ assert scheduled.status == TaskStatus.SCHEDULED
902
905
 
903
906
 
904
907
  def test_resolve_task_preserves_unmodelled_priority():
@@ -407,7 +407,7 @@ wheels = [
407
407
 
408
408
  [[package]]
409
409
  name = "qodev-apollo-api"
410
- version = "0.3.0"
410
+ version = "0.3.2"
411
411
  source = { editable = "." }
412
412
  dependencies = [
413
413
  { name = "httpx" },