qodev-apollo-api 0.2.1__tar.gz → 0.2.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.2.1 → qodev_apollo_api-0.2.2}/PKG-INFO +1 -1
  2. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/pyproject.toml +1 -1
  3. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/__init__.py +1 -1
  4. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/client.py +12 -1
  5. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/models.py +8 -2
  6. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_client.py +32 -0
  7. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_models.py +17 -0
  8. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/uv.lock +1 -1
  9. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.github/workflows/ci.yml +0 -0
  10. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.github/workflows/publish.yml +0 -0
  11. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.gitignore +0 -0
  12. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.pre-commit-config.yaml +0 -0
  13. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/CHANGELOG.md +0 -0
  14. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/CLAUDE.md +0 -0
  15. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/LICENSE +0 -0
  16. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/Makefile +0 -0
  17. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/README.md +0 -0
  18. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/exceptions.py +0 -0
  19. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/py.typed +0 -0
  20. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/utils.py +0 -0
  21. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/__init__.py +0 -0
  22. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/__init__.py +0 -0
  23. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/validate_all_models.py +0 -0
  24. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/validate_email_task_flow.py +0 -0
  25. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_exceptions.py +0 -0
  26. {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qodev-apollo-api
3
- Version: 0.2.1
3
+ Version: 0.2.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.2.1"
3
+ version = "0.2.2"
4
4
  description = "Async Python client for Apollo.io CRM API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -151,4 +151,4 @@ __all__ = [
151
151
  "resolve_task",
152
152
  ]
153
153
 
154
- __version__ = "0.2.0"
154
+ __version__ = "0.2.2"
@@ -3,12 +3,14 @@
3
3
  Type-safe async wrapper for Apollo.io API.
4
4
  """
5
5
 
6
+ import logging
6
7
  import os
7
8
  from datetime import datetime
8
9
  from types import TracebackType
9
10
  from typing import Any
10
11
 
11
12
  import httpx
13
+ from pydantic import ValidationError
12
14
 
13
15
  from .exceptions import APIError, AuthenticationError, RateLimitError
14
16
  from .models import (
@@ -40,6 +42,8 @@ from .models import (
40
42
  )
41
43
  from .utils import markdown_to_prosemirror, normalize_linkedin_url, prosemirror_to_markdown
42
44
 
45
+ logger = logging.getLogger(__name__)
46
+
43
47
 
44
48
  class ApolloClient:
45
49
  """Async Apollo.io API client with context manager support."""
@@ -709,7 +713,14 @@ class ApolloClient:
709
713
  data["multi_sort"] = [{field: {"order": order}} for field, order in sort]
710
714
  result = await self._post("/tasks/search", data)
711
715
 
712
- tasks = [resolve_task(t) for t in result.get("tasks", [])]
716
+ tasks: list[Task] = []
717
+ for raw in result.get("tasks", []):
718
+ try:
719
+ tasks.append(resolve_task(raw))
720
+ except ValidationError:
721
+ # A single structurally-invalid row (e.g. missing id) must not sink the whole
722
+ # page — skip it so the rest of the results stay usable.
723
+ logger.warning("Skipping unparseable task id=%s", raw.get("id"), exc_info=True)
713
724
  pagination = result.get("pagination", {})
714
725
 
715
726
  return PaginatedResponse[Task](
@@ -823,8 +823,14 @@ class BaseTask(ApolloModel):
823
823
  title: str | None = None
824
824
  subject: str | None = None
825
825
  type: TaskType | None = None
826
- priority: TaskPriority | None = None
827
- status: TaskStatus | None = None
826
+ # Kept as raw strings, not the TaskStatus/TaskPriority enums: Apollo returns values the
827
+ # library doesn't model (status: skipped/archived/…; priority occasionally outside
828
+ # high/medium/low). A strict enum here broke deserialization — a task with e.g.
829
+ # status="skipped" failed the discriminated union AND resolve_task's OtherTask fallback
830
+ # (which inherits these fields), so a single such row raised for the *whole* search page.
831
+ # Compare against TaskStatus.* / TaskPriority.* — StrEnum equality still holds.
832
+ priority: str | None = None
833
+ status: str | None = None
828
834
  due_at: datetime | None = None
829
835
  note: str | None = None
830
836
  answered: bool | None = None
@@ -305,6 +305,38 @@ async def test_search_tasks(client: ApolloClient):
305
305
  assert client._client.request.call_args[0] == ("POST", "/tasks/search")
306
306
 
307
307
 
308
+ async def test_search_tasks_returns_task_with_unmodelled_status(client: ApolloClient):
309
+ """Regression: a task whose status Apollo returns but the library doesn't model
310
+ (skipped/archived/…) is returned with the raw status — not dropped, not raised. This
311
+ is what broke the LinkedIn connect batch: a single skipped task sank the whole page."""
312
+ client._client.request.return_value = _make_response(
313
+ {
314
+ "tasks": [{"id": "t1", "type": "linkedin_step_connect", "status": "skipped"}],
315
+ "pagination": {"total_entries": 1},
316
+ }
317
+ )
318
+
319
+ result = await client.search_tasks()
320
+
321
+ assert len(result.items) == 1
322
+ assert result.items[0].status == "skipped"
323
+
324
+
325
+ async def test_search_tasks_skips_unparseable_row(client: ApolloClient):
326
+ """A structurally-invalid row (missing id) is skipped, not fatal — the rest of the page
327
+ is still returned."""
328
+ client._client.request.return_value = _make_response(
329
+ {
330
+ "tasks": [{"type": "call"}, {"id": "t2", "type": "call"}],
331
+ "pagination": {"total_entries": 2},
332
+ }
333
+ )
334
+
335
+ result = await client.search_tasks()
336
+
337
+ assert [t.id for t in result.items] == ["t2"]
338
+
339
+
308
340
  async def test_search_tasks_with_sort(client: ApolloClient):
309
341
  """Test search_tasks with sort parameter generates multi_sort payload."""
310
342
  client._client.request.return_value = _make_response(
@@ -60,6 +60,7 @@ from qodev_apollo_api.models import (
60
60
  PhoneEntry,
61
61
  Pipeline,
62
62
  Stage,
63
+ TaskStatus,
63
64
  TaskType,
64
65
  Technology,
65
66
  TranscriptSegment,
@@ -890,6 +891,22 @@ def test_resolve_task_unknown_type_falls_back_to_other_task():
890
891
  assert result.id == "1"
891
892
 
892
893
 
894
+ def test_resolve_task_preserves_unmodelled_status():
895
+ """A status Apollo returns but the library doesn't model (skipped/archived/…) must
896
+ deserialize as the raw string rather than raise — otherwise a single such task sinks a
897
+ whole tasks/search page (it fails the union AND the OtherTask fallback)."""
898
+ result = resolve_task({"id": "1", "type": "linkedin_step_connect", "status": "skipped"})
899
+ assert result.status == "skipped"
900
+ # comparison against the known-value enum still works via StrEnum equality
901
+ assert result.status != TaskStatus.SCHEDULED
902
+
903
+
904
+ def test_resolve_task_preserves_unmodelled_priority():
905
+ """Same guarantee for priority (values outside high/medium/low)."""
906
+ result = resolve_task({"id": "1", "type": "linkedin_step_connect", "priority": "none"})
907
+ assert result.priority == "none"
908
+
909
+
893
910
  def test_task_with_full_emailer_message():
894
911
  """Test EmailTask with complete emailer_message including scheduling fields."""
895
912
  task = EmailTask.model_validate(
@@ -407,7 +407,7 @@ wheels = [
407
407
 
408
408
  [[package]]
409
409
  name = "qodev-apollo-api"
410
- version = "0.2.1"
410
+ version = "0.2.2"
411
411
  source = { editable = "." }
412
412
  dependencies = [
413
413
  { name = "httpx" },