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.
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/PKG-INFO +1 -1
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/pyproject.toml +1 -1
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/__init__.py +1 -1
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/client.py +12 -1
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/models.py +8 -2
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_client.py +32 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_models.py +17 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/uv.lock +1 -1
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.github/workflows/ci.yml +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.github/workflows/publish.yml +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.gitignore +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/.pre-commit-config.yaml +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/CHANGELOG.md +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/CLAUDE.md +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/LICENSE +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/Makefile +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/README.md +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/exceptions.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/py.typed +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/src/qodev_apollo_api/utils.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/__init__.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/__init__.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/validate_all_models.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/validate_email_task_flow.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_exceptions.py +0 -0
- {qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/test_utils.py +0 -0
|
@@ -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
|
|
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
|
-
|
|
827
|
-
status:
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{qodev_apollo_api-0.2.1 → qodev_apollo_api-0.2.2}/tests/integration/validate_email_task_flow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|