airbyte-agent-greenhouse 0.17.48__py3-none-any.whl

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 (57) hide show
  1. airbyte_agent_greenhouse/__init__.py +105 -0
  2. airbyte_agent_greenhouse/_vendored/__init__.py +1 -0
  3. airbyte_agent_greenhouse/_vendored/connector_sdk/__init__.py +82 -0
  4. airbyte_agent_greenhouse/_vendored/connector_sdk/auth_strategies.py +1120 -0
  5. airbyte_agent_greenhouse/_vendored/connector_sdk/auth_template.py +135 -0
  6. airbyte_agent_greenhouse/_vendored/connector_sdk/cloud_utils/__init__.py +5 -0
  7. airbyte_agent_greenhouse/_vendored/connector_sdk/cloud_utils/client.py +213 -0
  8. airbyte_agent_greenhouse/_vendored/connector_sdk/connector_model_loader.py +965 -0
  9. airbyte_agent_greenhouse/_vendored/connector_sdk/constants.py +78 -0
  10. airbyte_agent_greenhouse/_vendored/connector_sdk/exceptions.py +23 -0
  11. airbyte_agent_greenhouse/_vendored/connector_sdk/executor/__init__.py +31 -0
  12. airbyte_agent_greenhouse/_vendored/connector_sdk/executor/hosted_executor.py +196 -0
  13. airbyte_agent_greenhouse/_vendored/connector_sdk/executor/local_executor.py +1724 -0
  14. airbyte_agent_greenhouse/_vendored/connector_sdk/executor/models.py +190 -0
  15. airbyte_agent_greenhouse/_vendored/connector_sdk/extensions.py +693 -0
  16. airbyte_agent_greenhouse/_vendored/connector_sdk/http/__init__.py +37 -0
  17. airbyte_agent_greenhouse/_vendored/connector_sdk/http/adapters/__init__.py +9 -0
  18. airbyte_agent_greenhouse/_vendored/connector_sdk/http/adapters/httpx_adapter.py +251 -0
  19. airbyte_agent_greenhouse/_vendored/connector_sdk/http/config.py +98 -0
  20. airbyte_agent_greenhouse/_vendored/connector_sdk/http/exceptions.py +119 -0
  21. airbyte_agent_greenhouse/_vendored/connector_sdk/http/protocols.py +114 -0
  22. airbyte_agent_greenhouse/_vendored/connector_sdk/http/response.py +104 -0
  23. airbyte_agent_greenhouse/_vendored/connector_sdk/http_client.py +693 -0
  24. airbyte_agent_greenhouse/_vendored/connector_sdk/introspection.py +262 -0
  25. airbyte_agent_greenhouse/_vendored/connector_sdk/logging/__init__.py +11 -0
  26. airbyte_agent_greenhouse/_vendored/connector_sdk/logging/logger.py +273 -0
  27. airbyte_agent_greenhouse/_vendored/connector_sdk/logging/types.py +93 -0
  28. airbyte_agent_greenhouse/_vendored/connector_sdk/observability/__init__.py +11 -0
  29. airbyte_agent_greenhouse/_vendored/connector_sdk/observability/config.py +179 -0
  30. airbyte_agent_greenhouse/_vendored/connector_sdk/observability/models.py +19 -0
  31. airbyte_agent_greenhouse/_vendored/connector_sdk/observability/redactor.py +81 -0
  32. airbyte_agent_greenhouse/_vendored/connector_sdk/observability/session.py +103 -0
  33. airbyte_agent_greenhouse/_vendored/connector_sdk/performance/__init__.py +6 -0
  34. airbyte_agent_greenhouse/_vendored/connector_sdk/performance/instrumentation.py +57 -0
  35. airbyte_agent_greenhouse/_vendored/connector_sdk/performance/metrics.py +93 -0
  36. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/__init__.py +75 -0
  37. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/base.py +164 -0
  38. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/components.py +239 -0
  39. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/connector.py +120 -0
  40. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/extensions.py +230 -0
  41. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/operations.py +146 -0
  42. airbyte_agent_greenhouse/_vendored/connector_sdk/schema/security.py +223 -0
  43. airbyte_agent_greenhouse/_vendored/connector_sdk/secrets.py +182 -0
  44. airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/__init__.py +10 -0
  45. airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/config.py +32 -0
  46. airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/events.py +59 -0
  47. airbyte_agent_greenhouse/_vendored/connector_sdk/telemetry/tracker.py +155 -0
  48. airbyte_agent_greenhouse/_vendored/connector_sdk/types.py +245 -0
  49. airbyte_agent_greenhouse/_vendored/connector_sdk/utils.py +60 -0
  50. airbyte_agent_greenhouse/_vendored/connector_sdk/validation.py +828 -0
  51. airbyte_agent_greenhouse/connector.py +1391 -0
  52. airbyte_agent_greenhouse/connector_model.py +2356 -0
  53. airbyte_agent_greenhouse/models.py +281 -0
  54. airbyte_agent_greenhouse/types.py +136 -0
  55. airbyte_agent_greenhouse-0.17.48.dist-info/METADATA +116 -0
  56. airbyte_agent_greenhouse-0.17.48.dist-info/RECORD +57 -0
  57. airbyte_agent_greenhouse-0.17.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,1391 @@
1
+ """
2
+ Greenhouse connector.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any, Callable, TypeVar, AsyncIterator, overload
9
+ try:
10
+ from typing import Literal
11
+ except ImportError:
12
+ from typing_extensions import Literal
13
+
14
+ from .connector_model import GreenhouseConnectorModel
15
+ from ._vendored.connector_sdk.introspection import describe_entities, generate_tool_description
16
+ from .types import (
17
+ ApplicationAttachmentDownloadParams,
18
+ ApplicationsGetParams,
19
+ ApplicationsListParams,
20
+ CandidateAttachmentDownloadParams,
21
+ CandidatesGetParams,
22
+ CandidatesListParams,
23
+ DepartmentsGetParams,
24
+ DepartmentsListParams,
25
+ JobPostsGetParams,
26
+ JobPostsListParams,
27
+ JobsGetParams,
28
+ JobsListParams,
29
+ OffersGetParams,
30
+ OffersListParams,
31
+ OfficesGetParams,
32
+ OfficesListParams,
33
+ ScheduledInterviewsGetParams,
34
+ ScheduledInterviewsListParams,
35
+ SourcesListParams,
36
+ UsersGetParams,
37
+ UsersListParams,
38
+ )
39
+ if TYPE_CHECKING:
40
+ from .models import GreenhouseAuthConfig
41
+ # Import response models and envelope models at runtime
42
+ from .models import (
43
+ GreenhouseExecuteResult,
44
+ GreenhouseExecuteResultWithMeta,
45
+ CandidatesListResult,
46
+ ApplicationsListResult,
47
+ JobsListResult,
48
+ OffersListResult,
49
+ UsersListResult,
50
+ DepartmentsListResult,
51
+ OfficesListResult,
52
+ JobPostsListResult,
53
+ SourcesListResult,
54
+ ScheduledInterviewsListResult,
55
+ Application,
56
+ Candidate,
57
+ Department,
58
+ Job,
59
+ JobPost,
60
+ Offer,
61
+ Office,
62
+ ScheduledInterview,
63
+ Source,
64
+ User,
65
+ )
66
+
67
+ # TypeVar for decorator type preservation
68
+ _F = TypeVar("_F", bound=Callable[..., Any])
69
+
70
+
71
+
72
+ class GreenhouseConnector:
73
+ """
74
+ Type-safe Greenhouse API connector.
75
+
76
+ Auto-generated from OpenAPI specification with full type safety.
77
+ """
78
+
79
+ connector_name = "greenhouse"
80
+ connector_version = "0.1.2"
81
+ vendored_sdk_version = "0.1.0" # Version of vendored connector-sdk
82
+
83
+ # Map of (entity, action) -> needs_envelope for envelope wrapping decision
84
+ _ENVELOPE_MAP = {
85
+ ("candidates", "list"): True,
86
+ ("candidates", "get"): None,
87
+ ("applications", "list"): True,
88
+ ("applications", "get"): None,
89
+ ("jobs", "list"): True,
90
+ ("jobs", "get"): None,
91
+ ("offers", "list"): True,
92
+ ("offers", "get"): None,
93
+ ("users", "list"): True,
94
+ ("users", "get"): None,
95
+ ("departments", "list"): True,
96
+ ("departments", "get"): None,
97
+ ("offices", "list"): True,
98
+ ("offices", "get"): None,
99
+ ("job_posts", "list"): True,
100
+ ("job_posts", "get"): None,
101
+ ("sources", "list"): True,
102
+ ("scheduled_interviews", "list"): True,
103
+ ("scheduled_interviews", "get"): None,
104
+ ("application_attachment", "download"): None,
105
+ ("candidate_attachment", "download"): None,
106
+ }
107
+
108
+ # Map of (entity, action) -> {python_param_name: api_param_name}
109
+ # Used to convert snake_case TypedDict keys to API parameter names in execute()
110
+ _PARAM_MAP = {
111
+ ('candidates', 'list'): {'per_page': 'per_page', 'page': 'page'},
112
+ ('candidates', 'get'): {'id': 'id'},
113
+ ('applications', 'list'): {'per_page': 'per_page', 'page': 'page', 'created_before': 'created_before', 'created_after': 'created_after', 'last_activity_after': 'last_activity_after', 'job_id': 'job_id', 'status': 'status'},
114
+ ('applications', 'get'): {'id': 'id'},
115
+ ('jobs', 'list'): {'per_page': 'per_page', 'page': 'page'},
116
+ ('jobs', 'get'): {'id': 'id'},
117
+ ('offers', 'list'): {'per_page': 'per_page', 'page': 'page', 'created_before': 'created_before', 'created_after': 'created_after', 'resolved_after': 'resolved_after'},
118
+ ('offers', 'get'): {'id': 'id'},
119
+ ('users', 'list'): {'per_page': 'per_page', 'page': 'page', 'created_before': 'created_before', 'created_after': 'created_after', 'updated_before': 'updated_before', 'updated_after': 'updated_after'},
120
+ ('users', 'get'): {'id': 'id'},
121
+ ('departments', 'list'): {'per_page': 'per_page', 'page': 'page'},
122
+ ('departments', 'get'): {'id': 'id'},
123
+ ('offices', 'list'): {'per_page': 'per_page', 'page': 'page'},
124
+ ('offices', 'get'): {'id': 'id'},
125
+ ('job_posts', 'list'): {'per_page': 'per_page', 'page': 'page', 'live': 'live', 'active': 'active'},
126
+ ('job_posts', 'get'): {'id': 'id'},
127
+ ('sources', 'list'): {'per_page': 'per_page', 'page': 'page'},
128
+ ('scheduled_interviews', 'list'): {'per_page': 'per_page', 'page': 'page', 'created_before': 'created_before', 'created_after': 'created_after', 'updated_before': 'updated_before', 'updated_after': 'updated_after', 'starts_after': 'starts_after', 'ends_before': 'ends_before'},
129
+ ('scheduled_interviews', 'get'): {'id': 'id'},
130
+ ('application_attachment', 'download'): {'id': 'id', 'attachment_index': 'attachment_index', 'range_header': 'range_header'},
131
+ ('candidate_attachment', 'download'): {'id': 'id', 'attachment_index': 'attachment_index', 'range_header': 'range_header'},
132
+ }
133
+
134
+ def __init__(
135
+ self,
136
+ auth_config: GreenhouseAuthConfig | None = None,
137
+ external_user_id: str | None = None,
138
+ airbyte_client_id: str | None = None,
139
+ airbyte_client_secret: str | None = None,
140
+ on_token_refresh: Any | None = None ):
141
+ """
142
+ Initialize a new greenhouse connector instance.
143
+
144
+ Supports both local and hosted execution modes:
145
+ - Local mode: Provide `auth_config` for direct API calls
146
+ - Hosted mode: Provide `external_user_id`, `airbyte_client_id`, and `airbyte_client_secret` for hosted execution
147
+
148
+ Args:
149
+ auth_config: Typed authentication configuration (required for local mode)
150
+ external_user_id: External user ID (required for hosted mode)
151
+ airbyte_client_id: Airbyte OAuth client ID (required for hosted mode)
152
+ airbyte_client_secret: Airbyte OAuth client secret (required for hosted mode)
153
+ on_token_refresh: Optional callback for OAuth2 token refresh persistence.
154
+ Called with new_tokens dict when tokens are refreshed. Can be sync or async.
155
+ Example: lambda tokens: save_to_database(tokens)
156
+ Examples:
157
+ # Local mode (direct API calls)
158
+ connector = GreenhouseConnector(auth_config=GreenhouseAuthConfig(api_key="..."))
159
+ # Hosted mode (executed on Airbyte cloud)
160
+ connector = GreenhouseConnector(
161
+ external_user_id="user-123",
162
+ airbyte_client_id="client_abc123",
163
+ airbyte_client_secret="secret_xyz789"
164
+ )
165
+
166
+ # Local mode with OAuth2 token refresh callback
167
+ def save_tokens(new_tokens: dict) -> None:
168
+ # Persist updated tokens to your storage (file, database, etc.)
169
+ with open("tokens.json", "w") as f:
170
+ json.dump(new_tokens, f)
171
+
172
+ connector = GreenhouseConnector(
173
+ auth_config=GreenhouseAuthConfig(access_token="...", refresh_token="..."),
174
+ on_token_refresh=save_tokens
175
+ )
176
+ """
177
+ # Hosted mode: external_user_id, airbyte_client_id, and airbyte_client_secret provided
178
+ if external_user_id and airbyte_client_id and airbyte_client_secret:
179
+ from ._vendored.connector_sdk.executor import HostedExecutor
180
+ self._executor = HostedExecutor(
181
+ external_user_id=external_user_id,
182
+ airbyte_client_id=airbyte_client_id,
183
+ airbyte_client_secret=airbyte_client_secret,
184
+ connector_definition_id=str(GreenhouseConnectorModel.id),
185
+ )
186
+ else:
187
+ # Local mode: auth_config required
188
+ if not auth_config:
189
+ raise ValueError(
190
+ "Either provide (external_user_id, airbyte_client_id, airbyte_client_secret) for hosted mode "
191
+ "or auth_config for local mode"
192
+ )
193
+
194
+ from ._vendored.connector_sdk.executor import LocalExecutor
195
+
196
+ # Build config_values dict from server variables
197
+ config_values = None
198
+
199
+ self._executor = LocalExecutor(
200
+ model=GreenhouseConnectorModel,
201
+ auth_config=auth_config.model_dump() if auth_config else None,
202
+ config_values=config_values,
203
+ on_token_refresh=on_token_refresh
204
+ )
205
+
206
+ # Update base_url with server variables if provided
207
+
208
+ # Initialize entity query objects
209
+ self.candidates = CandidatesQuery(self)
210
+ self.applications = ApplicationsQuery(self)
211
+ self.jobs = JobsQuery(self)
212
+ self.offers = OffersQuery(self)
213
+ self.users = UsersQuery(self)
214
+ self.departments = DepartmentsQuery(self)
215
+ self.offices = OfficesQuery(self)
216
+ self.job_posts = JobPostsQuery(self)
217
+ self.sources = SourcesQuery(self)
218
+ self.scheduled_interviews = ScheduledInterviewsQuery(self)
219
+ self.application_attachment = ApplicationAttachmentQuery(self)
220
+ self.candidate_attachment = CandidateAttachmentQuery(self)
221
+
222
+ # ===== TYPED EXECUTE METHOD (Recommended Interface) =====
223
+
224
+ @overload
225
+ async def execute(
226
+ self,
227
+ entity: Literal["candidates"],
228
+ action: Literal["list"],
229
+ params: "CandidatesListParams"
230
+ ) -> "CandidatesListResult": ...
231
+
232
+ @overload
233
+ async def execute(
234
+ self,
235
+ entity: Literal["candidates"],
236
+ action: Literal["get"],
237
+ params: "CandidatesGetParams"
238
+ ) -> "Candidate": ...
239
+
240
+ @overload
241
+ async def execute(
242
+ self,
243
+ entity: Literal["applications"],
244
+ action: Literal["list"],
245
+ params: "ApplicationsListParams"
246
+ ) -> "ApplicationsListResult": ...
247
+
248
+ @overload
249
+ async def execute(
250
+ self,
251
+ entity: Literal["applications"],
252
+ action: Literal["get"],
253
+ params: "ApplicationsGetParams"
254
+ ) -> "Application": ...
255
+
256
+ @overload
257
+ async def execute(
258
+ self,
259
+ entity: Literal["jobs"],
260
+ action: Literal["list"],
261
+ params: "JobsListParams"
262
+ ) -> "JobsListResult": ...
263
+
264
+ @overload
265
+ async def execute(
266
+ self,
267
+ entity: Literal["jobs"],
268
+ action: Literal["get"],
269
+ params: "JobsGetParams"
270
+ ) -> "Job": ...
271
+
272
+ @overload
273
+ async def execute(
274
+ self,
275
+ entity: Literal["offers"],
276
+ action: Literal["list"],
277
+ params: "OffersListParams"
278
+ ) -> "OffersListResult": ...
279
+
280
+ @overload
281
+ async def execute(
282
+ self,
283
+ entity: Literal["offers"],
284
+ action: Literal["get"],
285
+ params: "OffersGetParams"
286
+ ) -> "Offer": ...
287
+
288
+ @overload
289
+ async def execute(
290
+ self,
291
+ entity: Literal["users"],
292
+ action: Literal["list"],
293
+ params: "UsersListParams"
294
+ ) -> "UsersListResult": ...
295
+
296
+ @overload
297
+ async def execute(
298
+ self,
299
+ entity: Literal["users"],
300
+ action: Literal["get"],
301
+ params: "UsersGetParams"
302
+ ) -> "User": ...
303
+
304
+ @overload
305
+ async def execute(
306
+ self,
307
+ entity: Literal["departments"],
308
+ action: Literal["list"],
309
+ params: "DepartmentsListParams"
310
+ ) -> "DepartmentsListResult": ...
311
+
312
+ @overload
313
+ async def execute(
314
+ self,
315
+ entity: Literal["departments"],
316
+ action: Literal["get"],
317
+ params: "DepartmentsGetParams"
318
+ ) -> "Department": ...
319
+
320
+ @overload
321
+ async def execute(
322
+ self,
323
+ entity: Literal["offices"],
324
+ action: Literal["list"],
325
+ params: "OfficesListParams"
326
+ ) -> "OfficesListResult": ...
327
+
328
+ @overload
329
+ async def execute(
330
+ self,
331
+ entity: Literal["offices"],
332
+ action: Literal["get"],
333
+ params: "OfficesGetParams"
334
+ ) -> "Office": ...
335
+
336
+ @overload
337
+ async def execute(
338
+ self,
339
+ entity: Literal["job_posts"],
340
+ action: Literal["list"],
341
+ params: "JobPostsListParams"
342
+ ) -> "JobPostsListResult": ...
343
+
344
+ @overload
345
+ async def execute(
346
+ self,
347
+ entity: Literal["job_posts"],
348
+ action: Literal["get"],
349
+ params: "JobPostsGetParams"
350
+ ) -> "JobPost": ...
351
+
352
+ @overload
353
+ async def execute(
354
+ self,
355
+ entity: Literal["sources"],
356
+ action: Literal["list"],
357
+ params: "SourcesListParams"
358
+ ) -> "SourcesListResult": ...
359
+
360
+ @overload
361
+ async def execute(
362
+ self,
363
+ entity: Literal["scheduled_interviews"],
364
+ action: Literal["list"],
365
+ params: "ScheduledInterviewsListParams"
366
+ ) -> "ScheduledInterviewsListResult": ...
367
+
368
+ @overload
369
+ async def execute(
370
+ self,
371
+ entity: Literal["scheduled_interviews"],
372
+ action: Literal["get"],
373
+ params: "ScheduledInterviewsGetParams"
374
+ ) -> "ScheduledInterview": ...
375
+
376
+ @overload
377
+ async def execute(
378
+ self,
379
+ entity: Literal["application_attachment"],
380
+ action: Literal["download"],
381
+ params: "ApplicationAttachmentDownloadParams"
382
+ ) -> "AsyncIterator[bytes]": ...
383
+
384
+ @overload
385
+ async def execute(
386
+ self,
387
+ entity: Literal["candidate_attachment"],
388
+ action: Literal["download"],
389
+ params: "CandidateAttachmentDownloadParams"
390
+ ) -> "AsyncIterator[bytes]": ...
391
+
392
+
393
+ @overload
394
+ async def execute(
395
+ self,
396
+ entity: str,
397
+ action: str,
398
+ params: dict[str, Any]
399
+ ) -> GreenhouseExecuteResult[Any] | GreenhouseExecuteResultWithMeta[Any, Any] | Any: ...
400
+
401
+ async def execute(
402
+ self,
403
+ entity: str,
404
+ action: str,
405
+ params: dict[str, Any] | None = None
406
+ ) -> Any:
407
+ """
408
+ Execute an entity operation with full type safety.
409
+
410
+ This is the recommended interface for blessed connectors as it:
411
+ - Uses the same signature as non-blessed connectors
412
+ - Provides full IDE autocomplete for entity/action/params
413
+ - Makes migration from generic to blessed connectors seamless
414
+
415
+ Args:
416
+ entity: Entity name (e.g., "customers")
417
+ action: Operation action (e.g., "create", "get", "list")
418
+ params: Operation parameters (typed based on entity+action)
419
+
420
+ Returns:
421
+ Typed response based on the operation
422
+
423
+ Example:
424
+ customer = await connector.execute(
425
+ entity="customers",
426
+ action="get",
427
+ params={"id": "cus_123"}
428
+ )
429
+ """
430
+ from ._vendored.connector_sdk.executor import ExecutionConfig
431
+
432
+ # Remap parameter names from snake_case (TypedDict keys) to API parameter names
433
+ if params:
434
+ param_map = self._PARAM_MAP.get((entity, action), {})
435
+ if param_map:
436
+ params = {param_map.get(k, k): v for k, v in params.items()}
437
+
438
+ # Use ExecutionConfig for both local and hosted executors
439
+ config = ExecutionConfig(
440
+ entity=entity,
441
+ action=action,
442
+ params=params
443
+ )
444
+
445
+ result = await self._executor.execute(config)
446
+
447
+ if not result.success:
448
+ raise RuntimeError(f"Execution failed: {result.error}")
449
+
450
+ # Check if this operation has extractors configured
451
+ has_extractors = self._ENVELOPE_MAP.get((entity, action), False)
452
+
453
+ if has_extractors:
454
+ # With extractors - return Pydantic envelope with data and meta
455
+ if result.meta is not None:
456
+ return GreenhouseExecuteResultWithMeta[Any, Any](
457
+ data=result.data,
458
+ meta=result.meta
459
+ )
460
+ else:
461
+ return GreenhouseExecuteResult[Any](data=result.data)
462
+ else:
463
+ # No extractors - return raw response data
464
+ return result.data
465
+
466
+ # ===== INTROSPECTION METHODS =====
467
+
468
+ @classmethod
469
+ def describe(cls, func: _F) -> _F:
470
+ """
471
+ Decorator that populates a function's docstring with connector capabilities.
472
+
473
+ This class method can be used as a decorator to automatically generate
474
+ comprehensive documentation for AI tool functions.
475
+
476
+ Usage:
477
+ @mcp.tool()
478
+ @GreenhouseConnector.describe
479
+ async def execute(entity: str, action: str, params: dict):
480
+ '''Execute operations.'''
481
+ ...
482
+
483
+ The decorated function's __doc__ will be updated with:
484
+ - Available entities and their actions
485
+ - Parameter signatures with required (*) and optional (?) markers
486
+ - Response structure documentation
487
+ - Example questions (if available in OpenAPI spec)
488
+
489
+ Args:
490
+ func: The function to decorate
491
+
492
+ Returns:
493
+ The same function with updated __doc__
494
+ """
495
+ description = generate_tool_description(GreenhouseConnectorModel)
496
+
497
+ original_doc = func.__doc__ or ""
498
+ if original_doc.strip():
499
+ func.__doc__ = f"{original_doc.strip()}\n{description}"
500
+ else:
501
+ func.__doc__ = description
502
+
503
+ return func
504
+
505
+ def list_entities(self) -> list[dict[str, Any]]:
506
+ """
507
+ Get structured data about available entities, actions, and parameters.
508
+
509
+ Returns a list of entity descriptions with:
510
+ - entity_name: Name of the entity (e.g., "contacts", "deals")
511
+ - description: Entity description from the first endpoint
512
+ - available_actions: List of actions (e.g., ["list", "get", "create"])
513
+ - parameters: Dict mapping action -> list of parameter dicts
514
+
515
+ Example:
516
+ entities = connector.list_entities()
517
+ for entity in entities:
518
+ print(f"{entity['entity_name']}: {entity['available_actions']}")
519
+ """
520
+ return describe_entities(GreenhouseConnectorModel)
521
+
522
+ def entity_schema(self, entity: str) -> dict[str, Any] | None:
523
+ """
524
+ Get the JSON schema for an entity.
525
+
526
+ Args:
527
+ entity: Entity name (e.g., "contacts", "companies")
528
+
529
+ Returns:
530
+ JSON schema dict describing the entity structure, or None if not found.
531
+
532
+ Example:
533
+ schema = connector.entity_schema("contacts")
534
+ if schema:
535
+ print(f"Contact properties: {list(schema.get('properties', {}).keys())}")
536
+ """
537
+ entity_def = next(
538
+ (e for e in GreenhouseConnectorModel.entities if e.name == entity),
539
+ None
540
+ )
541
+ if entity_def is None:
542
+ logging.getLogger(__name__).warning(
543
+ f"Entity '{entity}' not found. Available entities: "
544
+ f"{[e.name for e in GreenhouseConnectorModel.entities]}"
545
+ )
546
+ return entity_def.entity_schema if entity_def else None
547
+
548
+
549
+
550
+ class CandidatesQuery:
551
+ """
552
+ Query class for Candidates entity operations.
553
+ """
554
+
555
+ def __init__(self, connector: GreenhouseConnector):
556
+ """Initialize query with connector reference."""
557
+ self._connector = connector
558
+
559
+ async def list(
560
+ self,
561
+ per_page: int | None = None,
562
+ page: int | None = None,
563
+ **kwargs
564
+ ) -> CandidatesListResult:
565
+ """
566
+ Returns a paginated list of all candidates in the organization
567
+
568
+ Args:
569
+ per_page: Number of items to return per page (max 500)
570
+ page: Page number for pagination
571
+ **kwargs: Additional parameters
572
+
573
+ Returns:
574
+ CandidatesListResult
575
+ """
576
+ params = {k: v for k, v in {
577
+ "per_page": per_page,
578
+ "page": page,
579
+ **kwargs
580
+ }.items() if v is not None}
581
+
582
+ result = await self._connector.execute("candidates", "list", params)
583
+ # Cast generic envelope to concrete typed result
584
+ return CandidatesListResult(
585
+ data=result.data
586
+ )
587
+
588
+
589
+
590
+ async def get(
591
+ self,
592
+ id: str | None = None,
593
+ **kwargs
594
+ ) -> Candidate:
595
+ """
596
+ Get a single candidate by ID
597
+
598
+ Args:
599
+ id: Candidate ID
600
+ **kwargs: Additional parameters
601
+
602
+ Returns:
603
+ Candidate
604
+ """
605
+ params = {k: v for k, v in {
606
+ "id": id,
607
+ **kwargs
608
+ }.items() if v is not None}
609
+
610
+ result = await self._connector.execute("candidates", "get", params)
611
+ return result
612
+
613
+
614
+
615
+ class ApplicationsQuery:
616
+ """
617
+ Query class for Applications entity operations.
618
+ """
619
+
620
+ def __init__(self, connector: GreenhouseConnector):
621
+ """Initialize query with connector reference."""
622
+ self._connector = connector
623
+
624
+ async def list(
625
+ self,
626
+ per_page: int | None = None,
627
+ page: int | None = None,
628
+ created_before: str | None = None,
629
+ created_after: str | None = None,
630
+ last_activity_after: str | None = None,
631
+ job_id: int | None = None,
632
+ status: str | None = None,
633
+ **kwargs
634
+ ) -> ApplicationsListResult:
635
+ """
636
+ Returns a paginated list of all applications
637
+
638
+ Args:
639
+ per_page: Number of items to return per page (max 500)
640
+ page: Page number for pagination
641
+ created_before: Filter by applications created before this timestamp
642
+ created_after: Filter by applications created after this timestamp
643
+ last_activity_after: Filter by applications with activity after this timestamp
644
+ job_id: Filter by job ID
645
+ status: Filter by application status
646
+ **kwargs: Additional parameters
647
+
648
+ Returns:
649
+ ApplicationsListResult
650
+ """
651
+ params = {k: v for k, v in {
652
+ "per_page": per_page,
653
+ "page": page,
654
+ "created_before": created_before,
655
+ "created_after": created_after,
656
+ "last_activity_after": last_activity_after,
657
+ "job_id": job_id,
658
+ "status": status,
659
+ **kwargs
660
+ }.items() if v is not None}
661
+
662
+ result = await self._connector.execute("applications", "list", params)
663
+ # Cast generic envelope to concrete typed result
664
+ return ApplicationsListResult(
665
+ data=result.data
666
+ )
667
+
668
+
669
+
670
+ async def get(
671
+ self,
672
+ id: str | None = None,
673
+ **kwargs
674
+ ) -> Application:
675
+ """
676
+ Get a single application by ID
677
+
678
+ Args:
679
+ id: Application ID
680
+ **kwargs: Additional parameters
681
+
682
+ Returns:
683
+ Application
684
+ """
685
+ params = {k: v for k, v in {
686
+ "id": id,
687
+ **kwargs
688
+ }.items() if v is not None}
689
+
690
+ result = await self._connector.execute("applications", "get", params)
691
+ return result
692
+
693
+
694
+
695
+ class JobsQuery:
696
+ """
697
+ Query class for Jobs entity operations.
698
+ """
699
+
700
+ def __init__(self, connector: GreenhouseConnector):
701
+ """Initialize query with connector reference."""
702
+ self._connector = connector
703
+
704
+ async def list(
705
+ self,
706
+ per_page: int | None = None,
707
+ page: int | None = None,
708
+ **kwargs
709
+ ) -> JobsListResult:
710
+ """
711
+ Returns a paginated list of all jobs in the organization
712
+
713
+ Args:
714
+ per_page: Number of items to return per page (max 500)
715
+ page: Page number for pagination
716
+ **kwargs: Additional parameters
717
+
718
+ Returns:
719
+ JobsListResult
720
+ """
721
+ params = {k: v for k, v in {
722
+ "per_page": per_page,
723
+ "page": page,
724
+ **kwargs
725
+ }.items() if v is not None}
726
+
727
+ result = await self._connector.execute("jobs", "list", params)
728
+ # Cast generic envelope to concrete typed result
729
+ return JobsListResult(
730
+ data=result.data
731
+ )
732
+
733
+
734
+
735
+ async def get(
736
+ self,
737
+ id: str | None = None,
738
+ **kwargs
739
+ ) -> Job:
740
+ """
741
+ Get a single job by ID
742
+
743
+ Args:
744
+ id: Job ID
745
+ **kwargs: Additional parameters
746
+
747
+ Returns:
748
+ Job
749
+ """
750
+ params = {k: v for k, v in {
751
+ "id": id,
752
+ **kwargs
753
+ }.items() if v is not None}
754
+
755
+ result = await self._connector.execute("jobs", "get", params)
756
+ return result
757
+
758
+
759
+
760
+ class OffersQuery:
761
+ """
762
+ Query class for Offers entity operations.
763
+ """
764
+
765
+ def __init__(self, connector: GreenhouseConnector):
766
+ """Initialize query with connector reference."""
767
+ self._connector = connector
768
+
769
+ async def list(
770
+ self,
771
+ per_page: int | None = None,
772
+ page: int | None = None,
773
+ created_before: str | None = None,
774
+ created_after: str | None = None,
775
+ resolved_after: str | None = None,
776
+ **kwargs
777
+ ) -> OffersListResult:
778
+ """
779
+ Returns a paginated list of all offers
780
+
781
+ Args:
782
+ per_page: Number of items to return per page (max 500)
783
+ page: Page number for pagination
784
+ created_before: Filter by offers created before this timestamp
785
+ created_after: Filter by offers created after this timestamp
786
+ resolved_after: Filter by offers resolved after this timestamp
787
+ **kwargs: Additional parameters
788
+
789
+ Returns:
790
+ OffersListResult
791
+ """
792
+ params = {k: v for k, v in {
793
+ "per_page": per_page,
794
+ "page": page,
795
+ "created_before": created_before,
796
+ "created_after": created_after,
797
+ "resolved_after": resolved_after,
798
+ **kwargs
799
+ }.items() if v is not None}
800
+
801
+ result = await self._connector.execute("offers", "list", params)
802
+ # Cast generic envelope to concrete typed result
803
+ return OffersListResult(
804
+ data=result.data
805
+ )
806
+
807
+
808
+
809
+ async def get(
810
+ self,
811
+ id: str | None = None,
812
+ **kwargs
813
+ ) -> Offer:
814
+ """
815
+ Get a single offer by ID
816
+
817
+ Args:
818
+ id: Offer ID
819
+ **kwargs: Additional parameters
820
+
821
+ Returns:
822
+ Offer
823
+ """
824
+ params = {k: v for k, v in {
825
+ "id": id,
826
+ **kwargs
827
+ }.items() if v is not None}
828
+
829
+ result = await self._connector.execute("offers", "get", params)
830
+ return result
831
+
832
+
833
+
834
+ class UsersQuery:
835
+ """
836
+ Query class for Users entity operations.
837
+ """
838
+
839
+ def __init__(self, connector: GreenhouseConnector):
840
+ """Initialize query with connector reference."""
841
+ self._connector = connector
842
+
843
+ async def list(
844
+ self,
845
+ per_page: int | None = None,
846
+ page: int | None = None,
847
+ created_before: str | None = None,
848
+ created_after: str | None = None,
849
+ updated_before: str | None = None,
850
+ updated_after: str | None = None,
851
+ **kwargs
852
+ ) -> UsersListResult:
853
+ """
854
+ Returns a paginated list of all users
855
+
856
+ Args:
857
+ per_page: Number of items to return per page (max 500)
858
+ page: Page number for pagination
859
+ created_before: Filter by users created before this timestamp
860
+ created_after: Filter by users created after this timestamp
861
+ updated_before: Filter by users updated before this timestamp
862
+ updated_after: Filter by users updated after this timestamp
863
+ **kwargs: Additional parameters
864
+
865
+ Returns:
866
+ UsersListResult
867
+ """
868
+ params = {k: v for k, v in {
869
+ "per_page": per_page,
870
+ "page": page,
871
+ "created_before": created_before,
872
+ "created_after": created_after,
873
+ "updated_before": updated_before,
874
+ "updated_after": updated_after,
875
+ **kwargs
876
+ }.items() if v is not None}
877
+
878
+ result = await self._connector.execute("users", "list", params)
879
+ # Cast generic envelope to concrete typed result
880
+ return UsersListResult(
881
+ data=result.data
882
+ )
883
+
884
+
885
+
886
+ async def get(
887
+ self,
888
+ id: str | None = None,
889
+ **kwargs
890
+ ) -> User:
891
+ """
892
+ Get a single user by ID
893
+
894
+ Args:
895
+ id: User ID
896
+ **kwargs: Additional parameters
897
+
898
+ Returns:
899
+ User
900
+ """
901
+ params = {k: v for k, v in {
902
+ "id": id,
903
+ **kwargs
904
+ }.items() if v is not None}
905
+
906
+ result = await self._connector.execute("users", "get", params)
907
+ return result
908
+
909
+
910
+
911
+ class DepartmentsQuery:
912
+ """
913
+ Query class for Departments entity operations.
914
+ """
915
+
916
+ def __init__(self, connector: GreenhouseConnector):
917
+ """Initialize query with connector reference."""
918
+ self._connector = connector
919
+
920
+ async def list(
921
+ self,
922
+ per_page: int | None = None,
923
+ page: int | None = None,
924
+ **kwargs
925
+ ) -> DepartmentsListResult:
926
+ """
927
+ Returns a paginated list of all departments
928
+
929
+ Args:
930
+ per_page: Number of items to return per page (max 500)
931
+ page: Page number for pagination
932
+ **kwargs: Additional parameters
933
+
934
+ Returns:
935
+ DepartmentsListResult
936
+ """
937
+ params = {k: v for k, v in {
938
+ "per_page": per_page,
939
+ "page": page,
940
+ **kwargs
941
+ }.items() if v is not None}
942
+
943
+ result = await self._connector.execute("departments", "list", params)
944
+ # Cast generic envelope to concrete typed result
945
+ return DepartmentsListResult(
946
+ data=result.data
947
+ )
948
+
949
+
950
+
951
+ async def get(
952
+ self,
953
+ id: str | None = None,
954
+ **kwargs
955
+ ) -> Department:
956
+ """
957
+ Get a single department by ID
958
+
959
+ Args:
960
+ id: Department ID
961
+ **kwargs: Additional parameters
962
+
963
+ Returns:
964
+ Department
965
+ """
966
+ params = {k: v for k, v in {
967
+ "id": id,
968
+ **kwargs
969
+ }.items() if v is not None}
970
+
971
+ result = await self._connector.execute("departments", "get", params)
972
+ return result
973
+
974
+
975
+
976
+ class OfficesQuery:
977
+ """
978
+ Query class for Offices entity operations.
979
+ """
980
+
981
+ def __init__(self, connector: GreenhouseConnector):
982
+ """Initialize query with connector reference."""
983
+ self._connector = connector
984
+
985
+ async def list(
986
+ self,
987
+ per_page: int | None = None,
988
+ page: int | None = None,
989
+ **kwargs
990
+ ) -> OfficesListResult:
991
+ """
992
+ Returns a paginated list of all offices
993
+
994
+ Args:
995
+ per_page: Number of items to return per page (max 500)
996
+ page: Page number for pagination
997
+ **kwargs: Additional parameters
998
+
999
+ Returns:
1000
+ OfficesListResult
1001
+ """
1002
+ params = {k: v for k, v in {
1003
+ "per_page": per_page,
1004
+ "page": page,
1005
+ **kwargs
1006
+ }.items() if v is not None}
1007
+
1008
+ result = await self._connector.execute("offices", "list", params)
1009
+ # Cast generic envelope to concrete typed result
1010
+ return OfficesListResult(
1011
+ data=result.data
1012
+ )
1013
+
1014
+
1015
+
1016
+ async def get(
1017
+ self,
1018
+ id: str | None = None,
1019
+ **kwargs
1020
+ ) -> Office:
1021
+ """
1022
+ Get a single office by ID
1023
+
1024
+ Args:
1025
+ id: Office ID
1026
+ **kwargs: Additional parameters
1027
+
1028
+ Returns:
1029
+ Office
1030
+ """
1031
+ params = {k: v for k, v in {
1032
+ "id": id,
1033
+ **kwargs
1034
+ }.items() if v is not None}
1035
+
1036
+ result = await self._connector.execute("offices", "get", params)
1037
+ return result
1038
+
1039
+
1040
+
1041
+ class JobPostsQuery:
1042
+ """
1043
+ Query class for JobPosts entity operations.
1044
+ """
1045
+
1046
+ def __init__(self, connector: GreenhouseConnector):
1047
+ """Initialize query with connector reference."""
1048
+ self._connector = connector
1049
+
1050
+ async def list(
1051
+ self,
1052
+ per_page: int | None = None,
1053
+ page: int | None = None,
1054
+ live: bool | None = None,
1055
+ active: bool | None = None,
1056
+ **kwargs
1057
+ ) -> JobPostsListResult:
1058
+ """
1059
+ Returns a paginated list of all job posts
1060
+
1061
+ Args:
1062
+ per_page: Number of items to return per page (max 500)
1063
+ page: Page number for pagination
1064
+ live: Filter by live status
1065
+ active: Filter by active status
1066
+ **kwargs: Additional parameters
1067
+
1068
+ Returns:
1069
+ JobPostsListResult
1070
+ """
1071
+ params = {k: v for k, v in {
1072
+ "per_page": per_page,
1073
+ "page": page,
1074
+ "live": live,
1075
+ "active": active,
1076
+ **kwargs
1077
+ }.items() if v is not None}
1078
+
1079
+ result = await self._connector.execute("job_posts", "list", params)
1080
+ # Cast generic envelope to concrete typed result
1081
+ return JobPostsListResult(
1082
+ data=result.data
1083
+ )
1084
+
1085
+
1086
+
1087
+ async def get(
1088
+ self,
1089
+ id: str | None = None,
1090
+ **kwargs
1091
+ ) -> JobPost:
1092
+ """
1093
+ Get a single job post by ID
1094
+
1095
+ Args:
1096
+ id: Job Post ID
1097
+ **kwargs: Additional parameters
1098
+
1099
+ Returns:
1100
+ JobPost
1101
+ """
1102
+ params = {k: v for k, v in {
1103
+ "id": id,
1104
+ **kwargs
1105
+ }.items() if v is not None}
1106
+
1107
+ result = await self._connector.execute("job_posts", "get", params)
1108
+ return result
1109
+
1110
+
1111
+
1112
+ class SourcesQuery:
1113
+ """
1114
+ Query class for Sources entity operations.
1115
+ """
1116
+
1117
+ def __init__(self, connector: GreenhouseConnector):
1118
+ """Initialize query with connector reference."""
1119
+ self._connector = connector
1120
+
1121
+ async def list(
1122
+ self,
1123
+ per_page: int | None = None,
1124
+ page: int | None = None,
1125
+ **kwargs
1126
+ ) -> SourcesListResult:
1127
+ """
1128
+ Returns a paginated list of all sources
1129
+
1130
+ Args:
1131
+ per_page: Number of items to return per page (max 500)
1132
+ page: Page number for pagination
1133
+ **kwargs: Additional parameters
1134
+
1135
+ Returns:
1136
+ SourcesListResult
1137
+ """
1138
+ params = {k: v for k, v in {
1139
+ "per_page": per_page,
1140
+ "page": page,
1141
+ **kwargs
1142
+ }.items() if v is not None}
1143
+
1144
+ result = await self._connector.execute("sources", "list", params)
1145
+ # Cast generic envelope to concrete typed result
1146
+ return SourcesListResult(
1147
+ data=result.data
1148
+ )
1149
+
1150
+
1151
+
1152
+ class ScheduledInterviewsQuery:
1153
+ """
1154
+ Query class for ScheduledInterviews entity operations.
1155
+ """
1156
+
1157
+ def __init__(self, connector: GreenhouseConnector):
1158
+ """Initialize query with connector reference."""
1159
+ self._connector = connector
1160
+
1161
+ async def list(
1162
+ self,
1163
+ per_page: int | None = None,
1164
+ page: int | None = None,
1165
+ created_before: str | None = None,
1166
+ created_after: str | None = None,
1167
+ updated_before: str | None = None,
1168
+ updated_after: str | None = None,
1169
+ starts_after: str | None = None,
1170
+ ends_before: str | None = None,
1171
+ **kwargs
1172
+ ) -> ScheduledInterviewsListResult:
1173
+ """
1174
+ Returns a paginated list of all scheduled interviews
1175
+
1176
+ Args:
1177
+ per_page: Number of items to return per page (max 500)
1178
+ page: Page number for pagination
1179
+ created_before: Filter by interviews created before this timestamp
1180
+ created_after: Filter by interviews created after this timestamp
1181
+ updated_before: Filter by interviews updated before this timestamp
1182
+ updated_after: Filter by interviews updated after this timestamp
1183
+ starts_after: Filter by interviews starting after this timestamp
1184
+ ends_before: Filter by interviews ending before this timestamp
1185
+ **kwargs: Additional parameters
1186
+
1187
+ Returns:
1188
+ ScheduledInterviewsListResult
1189
+ """
1190
+ params = {k: v for k, v in {
1191
+ "per_page": per_page,
1192
+ "page": page,
1193
+ "created_before": created_before,
1194
+ "created_after": created_after,
1195
+ "updated_before": updated_before,
1196
+ "updated_after": updated_after,
1197
+ "starts_after": starts_after,
1198
+ "ends_before": ends_before,
1199
+ **kwargs
1200
+ }.items() if v is not None}
1201
+
1202
+ result = await self._connector.execute("scheduled_interviews", "list", params)
1203
+ # Cast generic envelope to concrete typed result
1204
+ return ScheduledInterviewsListResult(
1205
+ data=result.data
1206
+ )
1207
+
1208
+
1209
+
1210
+ async def get(
1211
+ self,
1212
+ id: str | None = None,
1213
+ **kwargs
1214
+ ) -> ScheduledInterview:
1215
+ """
1216
+ Get a single scheduled interview by ID
1217
+
1218
+ Args:
1219
+ id: Scheduled Interview ID
1220
+ **kwargs: Additional parameters
1221
+
1222
+ Returns:
1223
+ ScheduledInterview
1224
+ """
1225
+ params = {k: v for k, v in {
1226
+ "id": id,
1227
+ **kwargs
1228
+ }.items() if v is not None}
1229
+
1230
+ result = await self._connector.execute("scheduled_interviews", "get", params)
1231
+ return result
1232
+
1233
+
1234
+
1235
+ class ApplicationAttachmentQuery:
1236
+ """
1237
+ Query class for ApplicationAttachment entity operations.
1238
+ """
1239
+
1240
+ def __init__(self, connector: GreenhouseConnector):
1241
+ """Initialize query with connector reference."""
1242
+ self._connector = connector
1243
+
1244
+ async def download(
1245
+ self,
1246
+ attachment_index: str,
1247
+ id: str | None = None,
1248
+ range_header: str | None = None,
1249
+ **kwargs
1250
+ ) -> AsyncIterator[bytes]:
1251
+ """
1252
+ Downloads an attachment (resume, cover letter, etc.) for an application by index.
1253
+ The attachment URL is a temporary signed AWS S3 URL that expires within 7 days.
1254
+ Files should be downloaded immediately after retrieval.
1255
+
1256
+
1257
+ Args:
1258
+ id: Application ID
1259
+ attachment_index: Index of the attachment to download (0-based)
1260
+ range_header: Optional Range header for partial downloads (e.g., 'bytes=0-99')
1261
+ **kwargs: Additional parameters
1262
+
1263
+ Returns:
1264
+ AsyncIterator[bytes]
1265
+ """
1266
+ params = {k: v for k, v in {
1267
+ "id": id,
1268
+ "attachment_index": attachment_index,
1269
+ "range_header": range_header,
1270
+ **kwargs
1271
+ }.items() if v is not None}
1272
+
1273
+ result = await self._connector.execute("application_attachment", "download", params)
1274
+ return result
1275
+
1276
+
1277
+ async def download_local(
1278
+ self,
1279
+ attachment_index: str,
1280
+ path: str,
1281
+ id: str | None = None,
1282
+ range_header: str | None = None,
1283
+ **kwargs
1284
+ ) -> Path:
1285
+ """
1286
+ Downloads an attachment (resume, cover letter, etc.) for an application by index.
1287
+ The attachment URL is a temporary signed AWS S3 URL that expires within 7 days.
1288
+ Files should be downloaded immediately after retrieval.
1289
+ and save to file.
1290
+
1291
+ Args:
1292
+ id: Application ID
1293
+ attachment_index: Index of the attachment to download (0-based)
1294
+ range_header: Optional Range header for partial downloads (e.g., 'bytes=0-99')
1295
+ path: File path to save downloaded content
1296
+ **kwargs: Additional parameters
1297
+
1298
+ Returns:
1299
+ str: Path to the downloaded file
1300
+ """
1301
+ from ._vendored.connector_sdk import save_download
1302
+
1303
+ # Get the async iterator
1304
+ content_iterator = await self.download(
1305
+ id=id,
1306
+ attachment_index=attachment_index,
1307
+ range_header=range_header,
1308
+ **kwargs
1309
+ )
1310
+
1311
+ return await save_download(content_iterator, path)
1312
+
1313
+
1314
+ class CandidateAttachmentQuery:
1315
+ """
1316
+ Query class for CandidateAttachment entity operations.
1317
+ """
1318
+
1319
+ def __init__(self, connector: GreenhouseConnector):
1320
+ """Initialize query with connector reference."""
1321
+ self._connector = connector
1322
+
1323
+ async def download(
1324
+ self,
1325
+ attachment_index: str,
1326
+ id: str | None = None,
1327
+ range_header: str | None = None,
1328
+ **kwargs
1329
+ ) -> AsyncIterator[bytes]:
1330
+ """
1331
+ Downloads an attachment (resume, cover letter, etc.) for a candidate by index.
1332
+ The attachment URL is a temporary signed AWS S3 URL that expires within 7 days.
1333
+ Files should be downloaded immediately after retrieval.
1334
+
1335
+
1336
+ Args:
1337
+ id: Candidate ID
1338
+ attachment_index: Index of the attachment to download (0-based)
1339
+ range_header: Optional Range header for partial downloads (e.g., 'bytes=0-99')
1340
+ **kwargs: Additional parameters
1341
+
1342
+ Returns:
1343
+ AsyncIterator[bytes]
1344
+ """
1345
+ params = {k: v for k, v in {
1346
+ "id": id,
1347
+ "attachment_index": attachment_index,
1348
+ "range_header": range_header,
1349
+ **kwargs
1350
+ }.items() if v is not None}
1351
+
1352
+ result = await self._connector.execute("candidate_attachment", "download", params)
1353
+ return result
1354
+
1355
+
1356
+ async def download_local(
1357
+ self,
1358
+ attachment_index: str,
1359
+ path: str,
1360
+ id: str | None = None,
1361
+ range_header: str | None = None,
1362
+ **kwargs
1363
+ ) -> Path:
1364
+ """
1365
+ Downloads an attachment (resume, cover letter, etc.) for a candidate by index.
1366
+ The attachment URL is a temporary signed AWS S3 URL that expires within 7 days.
1367
+ Files should be downloaded immediately after retrieval.
1368
+ and save to file.
1369
+
1370
+ Args:
1371
+ id: Candidate ID
1372
+ attachment_index: Index of the attachment to download (0-based)
1373
+ range_header: Optional Range header for partial downloads (e.g., 'bytes=0-99')
1374
+ path: File path to save downloaded content
1375
+ **kwargs: Additional parameters
1376
+
1377
+ Returns:
1378
+ str: Path to the downloaded file
1379
+ """
1380
+ from ._vendored.connector_sdk import save_download
1381
+
1382
+ # Get the async iterator
1383
+ content_iterator = await self.download(
1384
+ id=id,
1385
+ attachment_index=attachment_index,
1386
+ range_header=range_header,
1387
+ **kwargs
1388
+ )
1389
+
1390
+ return await save_download(content_iterator, path)
1391
+