qodev-apollo-api 0.2.2__tar.gz → 0.3.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 (26) hide show
  1. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/CHANGELOG.md +8 -0
  2. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/PKG-INFO +1 -1
  3. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/pyproject.toml +1 -1
  4. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/__init__.py +2 -0
  5. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/client.py +42 -0
  6. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/models.py +15 -0
  7. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/test_client.py +48 -0
  8. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/uv.lock +1 -1
  9. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/.github/workflows/ci.yml +0 -0
  10. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/.github/workflows/publish.yml +0 -0
  11. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/.gitignore +0 -0
  12. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/.pre-commit-config.yaml +0 -0
  13. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/CLAUDE.md +0 -0
  14. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/LICENSE +0 -0
  15. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/Makefile +0 -0
  16. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/README.md +0 -0
  17. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/exceptions.py +0 -0
  18. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/py.typed +0 -0
  19. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/src/qodev_apollo_api/utils.py +0 -0
  20. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/__init__.py +0 -0
  21. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/integration/__init__.py +0 -0
  22. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/integration/validate_all_models.py +0 -0
  23. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/integration/validate_email_task_flow.py +0 -0
  24. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/test_exceptions.py +0 -0
  25. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/test_models.py +0 -0
  26. {qodev_apollo_api-0.2.2 → qodev_apollo_api-0.3.0}/tests/test_utils.py +0 -0
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-07-08
11
+
12
+ ### Added
13
+
14
+ - `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.
15
+ - `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`).
16
+ - `CustomField` model, exported from the package root.
17
+
10
18
  ## [0.2.1] - 2026-07-01
11
19
 
12
20
  ### 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.0
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.0"
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,
@@ -111,6 +112,7 @@ __all__ = [
111
112
  "CrmJob",
112
113
  "CrmNote",
113
114
  "Currency",
115
+ "CustomField",
114
116
  "Deal",
115
117
  "Email",
116
118
  "EmailParticipant",
@@ -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,
@@ -520,6 +521,47 @@ class ApolloClient:
520
521
  page=1,
521
522
  )
522
523
 
524
+ async def update_opportunity_roles(self, opportunity_id: str, roles: list[dict]) -> Deal:
525
+ """Set the contact roles on a deal/opportunity (undocumented endpoint).
526
+
527
+ This **replaces** the full set of contact roles on the opportunity, so
528
+ callers should pass the complete desired set (read the current roles from
529
+ ``get_deal(...).opportunity_contact_roles`` and modify them). Each entry is
530
+ a dict with ``contact_id`` and, optionally, ``opportunity_contact_role_type_id``
531
+ and ``is_primary``::
532
+
533
+ roles = [
534
+ {"contact_id": "abc", "opportunity_contact_role_type_id": "t1", "is_primary": True},
535
+ {"contact_id": "def", "is_primary": False},
536
+ ]
537
+
538
+ Args:
539
+ opportunity_id: The opportunity/deal ID.
540
+ roles: The complete list of contact-role entries to set.
541
+
542
+ Returns:
543
+ The updated Deal.
544
+ """
545
+ data = {"opportunity_id": opportunity_id, "roles": roles}
546
+ result = await self._post("/opportunities/update_roles", data)
547
+ return Deal.model_validate(result.get("opportunity", result))
548
+
549
+ # ========================================================================
550
+ # CUSTOM FIELDS
551
+ # ========================================================================
552
+
553
+ async def list_custom_fields(self) -> list[CustomField]:
554
+ """List all typed custom field definitions (undocumented endpoint).
555
+
556
+ Returns definitions for custom fields across every modality (contact,
557
+ account, opportunity). Use ``modality`` to filter client-side.
558
+
559
+ Returns:
560
+ List of CustomField definitions.
561
+ """
562
+ result = await self._get("/typed_custom_fields")
563
+ return [CustomField.model_validate(f) for f in result.get("typed_custom_fields", [])]
564
+
523
565
  # ========================================================================
524
566
  # ENRICHMENT
525
567
  # ========================================================================
@@ -640,6 +640,21 @@ class Stage(ApolloModel):
640
640
  team_id: str | None = None
641
641
 
642
642
 
643
+ class CustomField(ApolloModel):
644
+ """A typed custom field definition (from GET /typed_custom_fields).
645
+
646
+ ``modality`` is the entity the field belongs to (e.g. "contact", "account",
647
+ "opportunity"); ``type`` is the value type (e.g. "text", "date", "picklist").
648
+ """
649
+
650
+ id: str
651
+ modality: str | None = None
652
+ name: str | None = None
653
+ type: str | None = None
654
+ picklist_options: list[str] = Field(default_factory=list)
655
+ mapped_crm_field: str | None = None
656
+
657
+
643
658
  # ============================================================================
644
659
  # ACTIVITY MODELS
645
660
  # ============================================================================
@@ -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.0"
411
411
  source = { editable = "." }
412
412
  dependencies = [
413
413
  { name = "httpx" },