qodev-apollo-api 0.2.2__tar.gz → 0.3.1__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.2 → qodev_apollo_api-0.3.1}/CHANGELOG.md +14 -0
  2. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/PKG-INFO +1 -1
  3. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/pyproject.toml +1 -1
  4. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/__init__.py +4 -0
  5. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/client.py +45 -0
  6. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/models.py +28 -1
  7. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/test_client.py +48 -0
  8. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/uv.lock +1 -1
  9. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/.github/workflows/ci.yml +0 -0
  10. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/.github/workflows/publish.yml +0 -0
  11. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/.gitignore +0 -0
  12. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/.pre-commit-config.yaml +0 -0
  13. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/CLAUDE.md +0 -0
  14. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/LICENSE +0 -0
  15. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/Makefile +0 -0
  16. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/README.md +0 -0
  17. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/exceptions.py +0 -0
  18. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/py.typed +0 -0
  19. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/src/qodev_apollo_api/utils.py +0 -0
  20. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/__init__.py +0 -0
  21. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/integration/__init__.py +0 -0
  22. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/integration/validate_all_models.py +0 -0
  23. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/integration/validate_email_task_flow.py +0 -0
  24. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/test_exceptions.py +0 -0
  25. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/test_models.py +0 -0
  26. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.1}/tests/test_utils.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-07-08
11
+
12
+ ### Changed
13
+
14
+ - `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.
15
+
16
+ ## [0.3.0] - 2026-07-08
17
+
18
+ ### Added
19
+
20
+ - `ApolloClient.update_opportunity_roles(opportunity_id, roles)` — `POST /opportunities/update_roles`. Sets the contact roles on a deal (replaces the full set; read the current roles from `get_deal(...).opportunity_contact_roles` and modify). Returns the updated `Deal`. Surfaces the previously curl-only role-management endpoint.
21
+ - `ApolloClient.list_custom_fields()` — `GET /typed_custom_fields`. Returns the account/contact/opportunity custom field definitions as a new `CustomField` model (`id`, `modality`, `name`, `type`, `picklist_options`, `mapped_crm_field`).
22
+ - `CustomField` model, exported from the package root.
23
+
10
24
  ## [0.2.1] - 2026-07-01
11
25
 
12
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qodev-apollo-api
3
- Version: 0.2.2
3
+ Version: 0.3.1
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.2"
3
+ version = "0.3.1"
4
4
  description = "Async Python client for Apollo.io CRM API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -35,6 +35,7 @@ from .models import (
35
35
  CrmJob,
36
36
  CrmNote,
37
37
  Currency,
38
+ CustomField,
38
39
  Deal,
39
40
  Email,
40
41
  EmailerMessage,
@@ -62,6 +63,7 @@ from .models import (
62
63
  PaginatedResponse,
63
64
  PhoneEntry,
64
65
  Pipeline,
66
+ RoleAssignment,
65
67
  SortOrder,
66
68
  Stage,
67
69
  Task,
@@ -111,6 +113,7 @@ __all__ = [
111
113
  "CrmJob",
112
114
  "CrmNote",
113
115
  "Currency",
116
+ "CustomField",
114
117
  "Deal",
115
118
  "Email",
116
119
  "EmailParticipant",
@@ -139,6 +142,7 @@ __all__ = [
139
142
  "PhoneEntry",
140
143
  "Pipeline",
141
144
  "RateLimitError",
145
+ "RoleAssignment",
142
146
  "SortOrder",
143
147
  "Stage",
144
148
  "Task",
@@ -22,6 +22,7 @@ from .models import (
22
22
  ContactDetail,
23
23
  Conversation,
24
24
  ConversationDetail,
25
+ CustomField,
25
26
  Deal,
26
27
  Email,
27
28
  EmailerMessage,
@@ -32,6 +33,7 @@ from .models import (
32
33
  OpportunityContactRoleType,
33
34
  PaginatedResponse,
34
35
  Pipeline,
36
+ RoleAssignment,
35
37
  SortOrder,
36
38
  Stage,
37
39
  Task,
@@ -520,6 +522,49 @@ class ApolloClient:
520
522
  page=1,
521
523
  )
522
524
 
525
+ async def update_opportunity_roles(
526
+ self, opportunity_id: str, roles: list[RoleAssignment]
527
+ ) -> Deal:
528
+ """Set the contact roles on a deal/opportunity (undocumented endpoint).
529
+
530
+ This **replaces** the full set of contact roles on the opportunity, so
531
+ callers should pass the complete desired set (read the current roles from
532
+ ``get_deal(...).opportunity_contact_roles`` and modify them). Each entry is
533
+ a dict with ``contact_id`` and, optionally, ``opportunity_contact_role_type_id``
534
+ and ``is_primary``::
535
+
536
+ roles = [
537
+ {"contact_id": "abc", "opportunity_contact_role_type_id": "t1", "is_primary": True},
538
+ {"contact_id": "def", "is_primary": False},
539
+ ]
540
+
541
+ Args:
542
+ opportunity_id: The opportunity/deal ID.
543
+ roles: The complete list of contact-role entries to set.
544
+
545
+ Returns:
546
+ The updated Deal.
547
+ """
548
+ data = {"opportunity_id": opportunity_id, "roles": roles}
549
+ result = await self._post("/opportunities/update_roles", data)
550
+ return Deal.model_validate(result.get("opportunity", result))
551
+
552
+ # ========================================================================
553
+ # CUSTOM FIELDS
554
+ # ========================================================================
555
+
556
+ async def list_custom_fields(self) -> list[CustomField]:
557
+ """List all typed custom field definitions (undocumented endpoint).
558
+
559
+ Returns definitions for custom fields across every modality (contact,
560
+ account, opportunity). Use ``modality`` to filter client-side.
561
+
562
+ Returns:
563
+ List of CustomField definitions.
564
+ """
565
+ result = await self._get("/typed_custom_fields")
566
+ return [CustomField.model_validate(f) for f in result.get("typed_custom_fields", [])]
567
+
523
568
  # ========================================================================
524
569
  # ENRICHMENT
525
570
  # ========================================================================
@@ -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
 
@@ -640,6 +652,21 @@ class Stage(ApolloModel):
640
652
  team_id: str | None = None
641
653
 
642
654
 
655
+ class CustomField(ApolloModel):
656
+ """A typed custom field definition (from GET /typed_custom_fields).
657
+
658
+ ``modality`` is the entity the field belongs to (e.g. "contact", "account",
659
+ "opportunity"); ``type`` is the value type (e.g. "text", "date", "picklist").
660
+ """
661
+
662
+ id: str
663
+ modality: str | None = None
664
+ name: str | None = None
665
+ type: str | None = None
666
+ picklist_options: list[str] = Field(default_factory=list)
667
+ mapped_crm_field: str | None = None
668
+
669
+
643
670
  # ============================================================================
644
671
  # ACTIVITY MODELS
645
672
  # ============================================================================
@@ -21,6 +21,7 @@ from qodev_apollo_api.models import (
21
21
  ContactDetail,
22
22
  Conversation,
23
23
  ConversationDetail,
24
+ CustomField,
24
25
  Deal,
25
26
  Email,
26
27
  EmailerMessage,
@@ -618,6 +619,53 @@ async def test_list_opportunity_contact_role_types(client: ApolloClient):
618
619
  assert call_args[1]["json"] == {}
619
620
 
620
621
 
622
+ async def test_update_opportunity_roles(client: ApolloClient):
623
+ """Test POST /opportunities/update_roles returns the updated Deal with the roles body."""
624
+ client._client.request.return_value = _make_response(
625
+ {"opportunity": {"id": "d1", "name": "Big Deal"}}
626
+ )
627
+
628
+ roles = [
629
+ {"contact_id": "c1", "opportunity_contact_role_type_id": "rt1", "is_primary": True},
630
+ {"contact_id": "c2", "is_primary": False},
631
+ ]
632
+ result = await client.update_opportunity_roles("d1", roles)
633
+
634
+ assert isinstance(result, Deal)
635
+ assert result.id == "d1"
636
+
637
+ call_args = client._client.request.call_args
638
+ assert call_args[0] == ("POST", "/opportunities/update_roles")
639
+ assert call_args[1]["json"] == {"opportunity_id": "d1", "roles": roles}
640
+
641
+
642
+ async def test_list_custom_fields(client: ApolloClient):
643
+ """Test GET /typed_custom_fields returns list[CustomField]."""
644
+ client._client.request.return_value = _make_response(
645
+ {
646
+ "typed_custom_fields": [
647
+ {"id": "f1", "modality": "contact", "name": "First Message", "type": "date"},
648
+ {
649
+ "id": "f2",
650
+ "modality": "opportunity",
651
+ "name": "Region",
652
+ "type": "picklist",
653
+ "picklist_options": ["EU", "US"],
654
+ },
655
+ ]
656
+ }
657
+ )
658
+
659
+ result = await client.list_custom_fields()
660
+
661
+ assert len(result) == 2
662
+ assert all(isinstance(f, CustomField) for f in result)
663
+ assert result[0].name == "First Message"
664
+ assert result[1].picklist_options == ["EU", "US"]
665
+
666
+ assert client._client.request.call_args[0] == ("GET", "/typed_custom_fields")
667
+
668
+
621
669
  async def test_get_contact_stages(client: ApolloClient):
622
670
  """Test GET /contact_stages returns list[dict]."""
623
671
  client._client.request.return_value = _make_response(
@@ -407,7 +407,7 @@ wheels = [
407
407
 
408
408
  [[package]]
409
409
  name = "qodev-apollo-api"
410
- version = "0.2.2"
410
+ version = "0.3.1"
411
411
  source = { editable = "." }
412
412
  dependencies = [
413
413
  { name = "httpx" },