1- from typing import Iterable , Dict , Any , List , cast
1+ from typing import Iterable , Dict , Any , List , Optional , Iterator , Union , cast
22
33import dlt
44from dlt .sources import DltResource
5- from dlt .sources .rest_api import rest_api_source
5+ from dlt .sources .rest_api import rest_api_source , RESTAPIConfig
6+ from dlt .sources .rest_api .typing import EndpointResource
7+
8+ from ..settings import ENTITIES_V2 , NESTED_ENTITIES_V2
9+
10+
11+ @dlt .source (name = "pipedrive_v2" )
12+ def pipedrive_v2_source (
13+ pipedrive_api_key : str = dlt .secrets .value ,
14+ company_domain : str = dlt .secrets .value ,
15+ resources : Optional [List [str ]] = None ,
16+ prefix : str = "v2_" ,
17+ ) -> Iterator [DltResource ]:
18+ """
19+ Get data from the Pipedrive API v2.
20+
21+ Args:
22+ pipedrive_api_key: API token for authentication
23+ company_domain: Your Pipedrive company domain
24+ resources: List of resource names to load (e.g., ["deals", "persons"]). If None, loads all available v2 resources.
25+ prefix: Prefix for table names (default: "v2_")
26+
27+ Returns:
28+ Resources for v2 endpoints. Nested endpoints (e.g., deal_products, deal_followers) are automatically included when their parent resource is selected.
29+
30+ See also: https://pipedrive.readme.io/docs/pipedrive-api-v2#api-v2-availability
31+ """
32+ resources = resources or list (ENTITIES_V2 .keys ())
33+
34+ # Filter valid v2 endpoints
35+ v2_resources_config = {
36+ resource : ENTITIES_V2 [resource ]
37+ for resource in resources
38+ if resource in ENTITIES_V2 # this ensures that resource is supported by v2 api
39+ }
40+
41+ if not v2_resources_config :
42+ raise ValueError (
43+ f"No valid v2 endpoints found in: { resources } . "
44+ f"Available endpoints: { list (ENTITIES_V2 .keys ())} "
45+ )
46+
47+ # Only include nested endpoints if their parent is in the v2 endpoints list
48+ nested_configs_to_create = {
49+ nested_name : nested_config
50+ for nested_name , nested_config in NESTED_ENTITIES_V2 .items ()
51+ if nested_config ["parent" ] in v2_resources_config
52+ }
53+
54+ # Create and yield v2 resources
55+ v2_resources = rest_v2_resources (
56+ pipedrive_api_key ,
57+ company_domain ,
58+ v2_resources_config ,
59+ nested_configs_to_create ,
60+ prefix ,
61+ )
62+ for resource in v2_resources :
63+ yield resource
664
765
866def rest_v2_resources (
@@ -16,40 +74,9 @@ def rest_v2_resources(
1674 Build and yield REST v2 resources for the given resource configurations.
1775 Includes nested endpoints that depend on parent resources.
1876 """
19-
77+ # Build resources list
2078 resources : List [Dict [str , Any ]] = []
2179
22- config : Dict [str , Any ] = {
23- "client" : {
24- "base_url" : f"https://{ company_domain } .pipedrive.com/api/v2/" ,
25- "auth" : {
26- "type" : "api_key" ,
27- "name" : "api_token" ,
28- "api_key" : pipedrive_api_key ,
29- "location" : "query" ,
30- },
31- },
32- "resource_defaults" : {
33- "primary_key" : "id" ,
34- "write_disposition" : "merge" ,
35- "endpoint" : {
36- "params" : {
37- "limit" : 500 ,
38- "sort_by" : "update_time" ,
39- "sort_direction" : "desc" ,
40- },
41- "data_selector" : "data" ,
42- "paginator" : {
43- "type" : "cursor" ,
44- "cursor_path" : "additional_data.next_cursor" ,
45- "cursor_param" : "cursor" ,
46- },
47- },
48- },
49- # IMPORTANT: bind the typed list here
50- "resources" : resources ,
51- }
52-
5380 # Build the resources list for the config from the provided resource configs
5481 for resource_name , endpoint_config in resource_configs .items ():
5582 resource_def : Dict [str , Any ] = {
@@ -63,7 +90,7 @@ def rest_v2_resources(
6390 parent_name = nested_config ["parent" ]
6491 endpoint_path = nested_config ["endpoint_path" ]
6592 params = nested_config .get ("params" , {})
66- primary_key = nested_config .get ("primary_key" , "id" )
93+ primary_key : Union [ str , List [ str ]] = nested_config .get ("primary_key" , "id" )
6794 include_from_parent = nested_config .get ("include_from_parent" )
6895
6996 # Use native rest_api_source nested endpoint syntax: {resources.parent_name.id}
@@ -80,10 +107,40 @@ def rest_v2_resources(
80107 nested_resource_def ["include_from_parent" ] = include_from_parent
81108 if primary_key != "id" :
82109 nested_resource_def ["primary_key" ] = primary_key
83-
84110 resources .append (nested_resource_def )
85111
86- api_source = rest_api_source (cast (Any , config ))
112+ # Create config with proper typing
113+ # Cast resources to the expected type since our Dict[str, Any] matches EndpointResource structure
114+ config : RESTAPIConfig = {
115+ "client" : {
116+ "base_url" : f"https://{ company_domain } .pipedrive.com/api/v2/" ,
117+ "auth" : {
118+ "type" : "api_key" ,
119+ "name" : "api_token" ,
120+ "api_key" : pipedrive_api_key ,
121+ "location" : "query" ,
122+ },
123+ },
124+ "resource_defaults" : {
125+ "primary_key" : "id" ,
126+ "write_disposition" : "merge" ,
127+ "endpoint" : {
128+ "params" : {
129+ "limit" : 500 ,
130+ "sort_by" : "update_time" ,
131+ "sort_direction" : "desc" ,
132+ },
133+ "data_selector" : "data" ,
134+ "paginator" : {
135+ "type" : "cursor" ,
136+ "cursor_path" : "additional_data.next_cursor" ,
137+ "cursor_param" : "cursor" ,
138+ },
139+ },
140+ },
141+ "resources" : cast (List [Union [str , EndpointResource , DltResource ]], resources ),
142+ }
87143
144+ api_source = rest_api_source (config )
88145 for resource in api_source .resources .values ():
89146 yield resource .with_name (f"{ prefix } { resource .name } " )
0 commit comments