Coverage for mindsdb / integrations / handlers / hubspot_handler / hubspot_tables.py: 0%
1379 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 00:36 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 00:36 +0000
1from typing import List, Dict, Text, Any, Optional, Tuple, Set, Iterable
3import pandas as pd
4from hubspot import HubSpot
5from hubspot.crm.objects import (
6 SimplePublicObjectId as HubSpotObjectId,
7 SimplePublicObjectBatchInput as HubSpotObjectBatchInput,
8 SimplePublicObjectInputForCreate as HubSpotObjectInputCreate,
9 BatchInputSimplePublicObjectBatchInputForCreate,
10 BatchInputSimplePublicObjectBatchInput,
11 BatchInputSimplePublicObjectId,
12)
13from mindsdb_sql_parser import ast as sql_ast
14from mindsdb_sql_parser.ast import ASTNode
16from mindsdb.integrations.utilities.handlers.query_utilities import UPDATEQueryExecutor, DELETEQueryExecutor
17from mindsdb.integrations.utilities.query_traversal import query_traversal
18from mindsdb.integrations.libs.api_handler import APIResource
19from mindsdb.integrations.utilities.sql_utils import FilterCondition, SortColumn, extract_comparison_conditions
20from mindsdb.utilities import log
22logger = log.getLogger(__name__)
25# Reference: https://developers.hubspot.com/docs/api-reference/crm-properties-v3/guide#create-unique-identifier-properties
26PROPERTY_ALIASES = {
27 "lastmodifieddate": "hs_lastmodifieddate",
28 "id": "hs_object_id",
29}
31REVERSE_PROPERTY_ALIASES = {value: key for key, value in PROPERTY_ALIASES.items()}
34def to_hubspot_property(col: str) -> str:
35 """Map internal column names to HubSpot property names."""
36 return PROPERTY_ALIASES.get(col, col)
39def to_internal_property(prop: str) -> str:
40 """Map HubSpot property names to internal column names."""
41 return REVERSE_PROPERTY_ALIASES.get(prop, prop)
44# Reference https://developers.hubspot.com/docs/api-reference/crm-properties-v3/guide#operators
45CANONICAL_OPERATOR_MAP = {
46 "=": "eq",
47 "==": "eq",
48 "eq": "eq",
49 "!=": "neq",
50 "<>": "neq",
51 "ne": "neq",
52 "neq": "neq",
53 "<": "lt",
54 "lt": "lt",
55 "<=": "lte",
56 "lte": "lte",
57 ">": "gt",
58 "gt": "gt",
59 ">=": "gte",
60 "gte": "gte",
61 "in": "in",
62 "not in": "not_in",
63 "not_in": "not_in",
64}
66CANONICAL_TOKENS = set(CANONICAL_OPERATOR_MAP.values())
68OPERATOR_MAP = {token: token.upper() for token in CANONICAL_TOKENS}
70SQL_OPERATOR_MAP = {
71 "eq": "=",
72 "neq": "!=",
73 "lt": "<",
74 "lte": "<=",
75 "gt": ">",
76 "gte": ">=",
77 "in": "in",
78 "not_in": "not in",
79}
82def canonical_op(op: Any) -> str:
83 """Normalize operators to canonical tokens used across search and post-filtering."""
84 if hasattr(op, "value"):
85 op = op.value
86 op_str = str(op).strip().lower()
87 return CANONICAL_OPERATOR_MAP.get(op_str, op_str)
90HUBSPOT_TABLE_COLUMN_DEFINITIONS: Dict[str, List[Tuple[str, str, str]]] = {
91 "companies": [
92 ("name", "VARCHAR", "Company name"),
93 ("domain", "VARCHAR", "Company domain"),
94 ("industry", "VARCHAR", "Industry"),
95 ("city", "VARCHAR", "City"),
96 ("state", "VARCHAR", "State"),
97 ("phone", "VARCHAR", "Phone number"),
98 ("website", "VARCHAR", "Company website URL"),
99 ("address", "VARCHAR", "Street address"),
100 ("zip", "VARCHAR", "Postal code"),
101 ("numberofemployees", "INTEGER", "Employee count"),
102 ("annualrevenue", "DECIMAL", "Annual revenue"),
103 ("lifecyclestage", "VARCHAR", "Lifecycle stage"),
104 ("current_erp", "VARCHAR", "Current ERP system"),
105 ("current_erp_version", "VARCHAR", "Current ERP version"),
106 ("current_web_platform", "VARCHAR", "Current web platform"),
107 ("accounting_software", "VARCHAR", "Accounting software"),
108 ("credit_card_processor", "VARCHAR", "Credit card processor"),
109 ("data_integration_platform", "VARCHAR", "Data integration platform"),
110 ("marketing_platform", "VARCHAR", "Marketing automation platform"),
111 ("pos_software", "VARCHAR", "POS software"),
112 ("shipping_software", "VARCHAR", "Shipping software"),
113 ("tax_platform", "VARCHAR", "Tax platform"),
114 ("partner", "BOOLEAN", "Partner flag"),
115 ("partner_type", "VARCHAR", "Partner type"),
116 ("partnership_status", "VARCHAR", "Partnership status"),
117 ("partner_payout_ytd", "DECIMAL", "Partner payout YTD"),
118 ("partnership_commission", "DECIMAL", "Partnership commission YTD"),
119 ("total_customer_value", "DECIMAL", "Total customer value"),
120 ("total_revenue", "DECIMAL", "Total revenue"),
121 ("createdate", "TIMESTAMP", "Creation date"),
122 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
123 ],
124 "contacts": [
125 ("email", "VARCHAR", "Email address"),
126 ("firstname", "VARCHAR", "First name"),
127 ("lastname", "VARCHAR", "Last name"),
128 ("phone", "VARCHAR", "Phone number"),
129 ("mobilephone", "VARCHAR", "Mobile phone number"),
130 ("jobtitle", "VARCHAR", "Job title"),
131 ("company", "VARCHAR", "Associated company"),
132 ("city", "VARCHAR", "City"),
133 ("website", "VARCHAR", "Website URL"),
134 ("lifecyclestage", "VARCHAR", "Lifecycle stage"),
135 ("hs_lead_status", "VARCHAR", "Lead status"),
136 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
137 ("dc_contact", "BOOLEAN", "Direct Commerce contact indicator"),
138 ("current_ecommerce_platform", "VARCHAR", "Current ecommerce platform"),
139 ("departments", "VARCHAR", "Departments"),
140 ("demo__requested", "BOOLEAN", "Demo requested flag"),
141 ("linkedin_url", "VARCHAR", "LinkedIn profile URL"),
142 ("referral_name", "VARCHAR", "Referral name"),
143 ("referral_company_name", "VARCHAR", "Referral company name"),
144 ("notes_last_contacted", "TIMESTAMP", "Last contacted timestamp"),
145 ("notes_last_updated", "TIMESTAMP", "Last activity updated timestamp"),
146 ("notes_next_activity_date", "TIMESTAMP", "Next activity date"),
147 ("num_contacted_notes", "INTEGER", "Number of contacted notes"),
148 ("hs_sales_email_last_clicked", "TIMESTAMP", "Last sales email clicked"),
149 ("hs_sales_email_last_opened", "TIMESTAMP", "Last sales email opened"),
150 ("createdate", "TIMESTAMP", "Creation date"),
151 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
152 ],
153 "deals": [
154 ("dealname", "VARCHAR", "Deal name"),
155 ("amount", "DECIMAL", "Deal amount"),
156 ("dealstage", "VARCHAR", "Deal stage"),
157 ("pipeline", "VARCHAR", "Sales pipeline"),
158 ("closedate", "DATE", "Expected close date"),
159 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
160 ("closed_won_reason", "VARCHAR", "Reason deal was won"),
161 ("closed_lost_reason", "VARCHAR", "Reason deal was lost"),
162 ("lead_attribution", "VARCHAR", "Lead attribution"),
163 ("services_requested", "VARCHAR", "Services requested"),
164 ("platform", "VARCHAR", "Platform"),
165 ("referral_partner", "VARCHAR", "Referral partner"),
166 ("referral_commission_amount", "DECIMAL", "Referral commission amount"),
167 ("tech_partners_involved", "VARCHAR", "Tech partners involved"),
168 ("sales_tier", "VARCHAR", "Sales tier"),
169 ("commission_status", "VARCHAR", "Commission status"),
170 ("createdate", "TIMESTAMP", "Creation date"),
171 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
172 ],
173 "tickets": [
174 ("subject", "VARCHAR", "Ticket subject"),
175 ("content", "TEXT", "Ticket content/description"),
176 ("hs_pipeline", "VARCHAR", "Pipeline"),
177 ("hs_pipeline_stage", "VARCHAR", "Pipeline stage"),
178 ("hs_ticket_priority", "VARCHAR", "Priority"),
179 ("hs_ticket_category", "VARCHAR", "Category"),
180 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
181 ("createdate", "TIMESTAMP", "Creation date"),
182 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
183 ],
184 "tasks": [
185 ("hs_task_subject", "VARCHAR", "Task subject"),
186 ("hs_task_body", "TEXT", "Task body/description"),
187 ("hs_task_status", "VARCHAR", "Task status"),
188 ("hs_task_priority", "VARCHAR", "Task priority"),
189 ("hs_task_type", "VARCHAR", "Task type"),
190 ("hs_timestamp", "TIMESTAMP", "Due date"),
191 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
192 ("createdate", "TIMESTAMP", "Creation date"),
193 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
194 ],
195 # Reference: https://developers.hubspot.com/docs/api-reference/crm-calls-v3/guide#create-a-call-engagement
196 "calls": [
197 ("hs_call_title", "VARCHAR", "Call title"),
198 ("hs_call_body", "TEXT", "Call notes/description"),
199 ("hs_call_direction", "VARCHAR", "Call direction (INBOUND/OUTBOUND)"),
200 ("hs_call_disposition", "VARCHAR", "Call outcome"),
201 ("hs_call_duration", "INTEGER", "Call duration in milliseconds"),
202 ("hs_call_status", "VARCHAR", "Call status"),
203 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
204 ("hs_timestamp", "TIMESTAMP", "Call timestamp"),
205 ("createdate", "TIMESTAMP", "Creation date"),
206 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
207 ],
208 # Reference: https://developers.hubspot.com/docs/api-reference/crm-emails-v3/guide#create-an-email-engagement
209 "emails": [
210 ("hs_email_subject", "VARCHAR", "Email subject"),
211 ("hs_email_text", "TEXT", "Email body text"),
212 ("hs_email_direction", "VARCHAR", "Email direction (INCOMING/FORWARDED/EMAIL)"),
213 ("hs_email_status", "VARCHAR", "Email status"),
214 ("hs_email_sender_email", "VARCHAR", "Sender email address"),
215 ("hs_email_to_email", "VARCHAR", "Recipient email address"),
216 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
217 ("hs_timestamp", "TIMESTAMP", "Email timestamp"),
218 ("createdate", "TIMESTAMP", "Creation date"),
219 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
220 ],
221 # Reference: https://developers.hubspot.com/docs/api-reference/crm-meetings-v3/guide#create-a-meeting-engagement
222 "meetings": [
223 ("hs_meeting_title", "VARCHAR", "Meeting title"),
224 ("hs_meeting_body", "TEXT", "Meeting description"),
225 ("hs_meeting_location", "VARCHAR", "Meeting location"),
226 ("hs_meeting_outcome", "VARCHAR", "Meeting outcome"),
227 ("hs_meeting_start_time", "TIMESTAMP", "Meeting start time"),
228 ("hs_meeting_end_time", "TIMESTAMP", "Meeting end time"),
229 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
230 ("hs_timestamp", "TIMESTAMP", "Meeting timestamp"),
231 ("createdate", "TIMESTAMP", "Creation date"),
232 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
233 ],
234 # Reference: https://developers.hubspot.com/docs/api-reference/crm-notes-v3/guide#create-a-note
235 "notes": [
236 ("hs_note_body", "TEXT", "Note content"),
237 ("hubspot_owner_id", "VARCHAR", "Owner ID"),
238 ("hs_timestamp", "TIMESTAMP", "Note timestamp"),
239 ("createdate", "TIMESTAMP", "Creation date"),
240 ("lastmodifieddate", "TIMESTAMP", "Last modification date"),
241 ],
242}
245def _extract_in_values(value: Any) -> List[Any]:
246 """
247 Extract values from IN clause, handling various formats:
248 - Python list/tuple/set: return as list
249 - AST Tuple node: extract values from args
250 - Single value: wrap in list
251 """
252 if hasattr(value, "args"):
253 extracted = []
254 for arg in value.args:
255 if hasattr(arg, "value"):
256 extracted.append(arg.value)
257 else:
258 extracted.append(arg)
259 return extracted
261 if isinstance(value, (list, tuple, set)):
262 return list(value)
264 return [value]
267def _extract_scalar_value(value: Any) -> Any:
268 """
269 Extract scalar value from AST Constant node or return as-is.
270 """
271 if hasattr(value, "value") and not hasattr(value, "args"):
272 return value.value
273 return value
276def _normalize_filter_conditions(conditions: Optional[List[FilterCondition]]) -> List[List[Any]]:
277 """
278 Convert FilterCondition instances into the condition format expected by query executors.
279 """
280 normalized: List[List[Any]] = []
281 if not conditions:
282 return normalized
284 for condition in conditions:
285 if isinstance(condition, FilterCondition):
286 op = canonical_op(condition.op)
287 col = to_internal_property(condition.column)
288 val = condition.value
290 # Check if this is an IN/NOT IN operator with AST Tuple
291 if op in ("in", "not_in") and hasattr(val, "args"):
292 val = _extract_in_values(val)
293 else:
294 val = _extract_scalar_value(val)
296 normalized.append([op, col, val])
297 elif isinstance(condition, (list, tuple)) and len(condition) >= 3:
298 normalized.append([canonical_op(condition[0]), to_internal_property(condition[1]), condition[2]])
299 return normalized
302def _normalize_conditions_for_executor(conditions: List[List[Any]]) -> List[List[Any]]:
303 normalized = []
304 for condition in conditions:
305 if len(condition) < 3:
306 continue
307 op, col, val = condition[0], condition[1], condition[2]
308 normalized.append([SQL_OPERATOR_MAP.get(op, op), col, val])
309 return normalized
312def _build_hubspot_search_filters(
313 conditions: List[List[Any]],
314 searchable_columns: Set[str],
315) -> Optional[List[Dict]]:
316 """
317 Convert normalized conditions to HubSpot Search API filter format.
318 Returns a list of filter dicts if all conditions are supported, otherwise None.
319 """
320 if not conditions:
321 return None
323 filters: List[Dict[str, Any]] = []
325 for condition in conditions:
326 if not isinstance(condition, (list, tuple)) or len(condition) < 3:
327 logger.debug(f"Invalid condition format: {condition}")
328 return None
330 operator, column, value = condition[0], condition[1], condition[2]
331 operator_key = canonical_op(operator)
333 if operator_key not in OPERATOR_MAP:
334 logger.debug(f"Unsupported operator '{operator_key}' for HubSpot search, falling back to post-filter")
335 return None
337 if column not in searchable_columns:
338 logger.debug(f"Column '{column}' not searchable in HubSpot, falling back to post-filter")
339 return None
341 property_name = to_hubspot_property(column)
343 hubspot_operator = OPERATOR_MAP[operator_key]
345 if hubspot_operator in {"IN", "NOT_IN"}:
346 values = _extract_in_values(value)
347 if not values:
348 logger.warning(f"Empty IN clause values for column '{column}'")
349 return None
351 logger.debug(f"Building IN filter for {column}: {values}")
352 filters.append(
353 {
354 "propertyName": property_name,
355 "operator": hubspot_operator,
356 "values": [str(val) for val in values],
357 }
358 )
359 else:
360 actual_value = _extract_scalar_value(value)
361 filters.append(
362 {
363 "propertyName": property_name,
364 "operator": hubspot_operator,
365 "value": str(actual_value),
366 }
367 )
369 if not filters:
370 return None
372 return filters
375def _build_hubspot_search_sorts(
376 sort_columns: List[SortColumn],
377 searchable_columns: Set[str],
378) -> Optional[List[Dict[str, Any]]]:
379 if not sort_columns:
380 return None
382 sorts: List[Dict[str, Any]] = []
383 for sort in sort_columns:
384 column = to_internal_property(sort.column)
385 if column not in searchable_columns:
386 logger.debug(f"Column '{column}' not sortable in HubSpot, falling back to post-sort")
387 return None
388 sorts.append(
389 {
390 "propertyName": to_hubspot_property(column),
391 "direction": "ASCENDING" if sort.ascending else "DESCENDING",
392 }
393 )
394 return sorts
397def _build_hubspot_properties(columns: Iterable[str]) -> List[str]:
398 properties = []
399 for col in columns:
400 prop = to_hubspot_property(col)
401 if prop == "hs_object_id":
402 continue
403 properties.append(prop)
404 return list(dict.fromkeys(properties))
407def _execute_hubspot_search(
408 search_api,
409 filters: List[Dict],
410 properties: List[str],
411 limit: Optional[int],
412 to_dict_fn: callable,
413 sorts: Optional[List[Dict[str, Any]]] = None,
414 object_type: Optional[str] = None,
415) -> List[Dict[str, Any]]:
416 """
417 Execute paginated HubSpot search with filters.
418 """
419 collected: List[Dict[str, Any]] = []
420 remaining = limit if limit is not None else float("inf")
421 after = None
423 while remaining > 0:
424 page_limit = min(int(remaining) if remaining != float("inf") else 200, 200)
425 search_request = {
426 "properties": properties,
427 "limit": page_limit,
428 }
430 if filters:
431 search_request["filterGroups"] = [{"filters": filters}]
432 if sorts:
433 search_request["sorts"] = sorts
435 if after is not None:
436 search_request["after"] = after
438 if object_type is None:
439 response = search_api.do_search(public_object_search_request=search_request)
440 else:
441 response = search_api.do_search(object_type, public_object_search_request=search_request)
443 results = getattr(response, "results", []) or []
444 for result in results:
445 collected.append(to_dict_fn(result))
446 if limit is not None and len(collected) >= limit:
447 return collected
449 paging = getattr(response, "paging", None)
450 next_page = getattr(paging, "next", None) if paging else None
451 after = getattr(next_page, "after", None) if next_page else None
453 if after is None:
454 break
456 if remaining != float("inf"):
457 remaining = limit - len(collected)
459 return collected
462class HubSpotAPIResource(APIResource):
463 """
464 Base class for HubSpot table resources with custom select handling.
466 Overrides the default select() method to properly handle server-side filtering
467 and avoid double-filtering issues with AST nodes.
468 """
470 # Reference: https://developers.hubspot.com/docs/api-reference/search/guide
471 SEARCHABLE_COLUMNS: Set[str] = set()
473 def select(self, query: ASTNode) -> pd.DataFrame:
474 """
475 Override select to handle server-side filtering properly.
476 """
477 conditions, order_by, result_limit = self._extract_query_params(query)
478 targets = self._get_targets(query)
479 original_targets = list(targets)
480 normalized_conditions = _normalize_filter_conditions(conditions)
481 self._validate_query_columns(targets, normalized_conditions, order_by)
483 filters = (
484 _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
485 if normalized_conditions
486 else None
487 )
488 sorts = _build_hubspot_search_sorts(order_by, self.SEARCHABLE_COLUMNS) if order_by else None
489 use_search = filters is not None or sorts is not None
491 fetch_columns = self._get_fetch_columns(
492 targets=targets,
493 normalized_conditions=normalized_conditions,
494 order_by=order_by,
495 use_search=use_search,
496 )
498 result = self.list(
499 conditions=conditions if not use_search else None,
500 limit=result_limit,
501 sort=order_by if not use_search else None,
502 targets=fetch_columns,
503 search_filters=filters,
504 search_sorts=sorts,
505 allow_search=use_search,
506 )
508 if use_search:
509 logger.debug("Filters/sorts pushed to HubSpot API, skipping post-filter/sort")
510 return self._apply_column_selection(result, original_targets)
512 if normalized_conditions and not result.empty:
513 result = self._apply_post_filter(result, normalized_conditions)
515 if order_by and not result.empty:
516 result = self._apply_post_sort(result, order_by)
518 return self._apply_column_selection(result, original_targets)
520 def _extract_query_params(self, query: ASTNode) -> Tuple[List, List, Optional[int]]:
521 """Extract conditions, order_by, and limit from query AST."""
522 conditions = extract_comparison_conditions(query.where) if query.where else []
524 order_by = []
525 if query.order_by:
526 for col in query.order_by:
527 ascending = True
528 if hasattr(col, "direction") and col.direction:
529 ascending = col.direction.upper() != "DESC"
530 elif hasattr(col, "ascending"):
531 ascending = col.ascending
532 order_by.append(SortColumn(col.field.parts[-1], ascending))
534 result_limit = query.limit.value if query.limit else None
536 return conditions, order_by, result_limit
538 def _get_targets(self, query: ASTNode) -> List[str]:
539 """Extract target column names from query."""
540 targets = []
541 if query.targets:
542 for target in query.targets:
543 if isinstance(target, sql_ast.Star):
544 continue
545 if isinstance(target, sql_ast.Identifier):
546 targets.append(to_internal_property(target.parts[-1]))
547 continue
548 targets.extend(self._extract_target_columns(target))
549 return list(dict.fromkeys(targets))
551 @staticmethod
552 def _extract_target_columns(target: ASTNode) -> List[str]:
553 columns: List[str] = []
555 def collect_identifiers(node, **kwargs):
556 if isinstance(node, sql_ast.Identifier):
557 columns.append(to_internal_property(node.parts[-1]))
558 return None
560 query_traversal(target, collect_identifiers)
561 return columns
563 def _apply_post_filter(self, df: pd.DataFrame, conditions: List[List[Any]]) -> pd.DataFrame:
564 """Apply post-filtering using pandas operations instead of SQL rendering."""
565 if df.empty:
566 return df
568 mask = pd.Series([True] * len(df), index=df.index)
570 for condition in conditions:
571 if len(condition) < 3:
572 continue
574 op, column, value = condition[0], condition[1], condition[2]
575 op_key = canonical_op(op)
577 if column not in df.columns:
578 logger.warning(f"Column '{column}' not found in DataFrame for post-filtering")
579 continue
581 try:
582 if op_key == "eq":
583 mask &= df[column] == value
584 elif op_key == "neq":
585 mask &= df[column] != value
586 elif op_key == "lt":
587 mask &= df[column] < value
588 elif op_key == "lte":
589 mask &= df[column] <= value
590 elif op_key == "gt":
591 mask &= df[column] > value
592 elif op_key == "gte":
593 mask &= df[column] >= value
594 elif op_key == "in":
595 values = value if isinstance(value, (list, tuple, set)) else [value]
596 mask &= df[column].isin(values)
597 elif op_key == "not_in":
598 values = value if isinstance(value, (list, tuple, set)) else [value]
599 mask &= ~df[column].isin(values)
600 except Exception as e:
601 logger.warning(f"Error applying post-filter for {column}: {e}")
602 continue
604 return df[mask].reset_index(drop=True)
606 def _apply_post_sort(self, df: pd.DataFrame, sort: List[SortColumn]) -> pd.DataFrame:
607 sort_columns = []
608 sort_ascending = []
609 for sort_item in sort:
610 column = to_internal_property(sort_item.column)
611 if column not in df.columns:
612 logger.warning(f"Column '{column}' not found in DataFrame for post-sorting")
613 continue
614 sort_columns.append(column)
615 sort_ascending.append(sort_item.ascending)
617 if not sort_columns:
618 return df
620 try:
621 return df.sort_values(by=sort_columns, ascending=sort_ascending).reset_index(drop=True)
622 except Exception as e:
623 logger.warning(f"Error applying post-sort: {e}")
624 return df
626 def _apply_column_selection(self, df: pd.DataFrame, targets: List[str]) -> pd.DataFrame:
627 """Apply column selection if specific columns requested."""
628 if not targets or df.empty:
629 return df
631 existing_targets = [t for t in targets if t in df.columns]
632 if existing_targets:
633 return df[existing_targets]
634 return df
636 def _validate_query_columns(
637 self,
638 targets: List[str],
639 normalized_conditions: List[List[Any]],
640 order_by: List[SortColumn],
641 ) -> None:
642 requested = set()
643 requested.update(targets or [])
645 for condition in normalized_conditions:
646 if len(condition) >= 2:
647 requested.add(condition[1])
649 for sort_item in order_by or []:
650 requested.add(to_internal_property(sort_item.column))
652 if not requested:
653 return
655 available = set(self.get_columns())
656 missing = [col for col in requested if col not in available]
657 if not missing:
658 return
660 missing_cols = ", ".join(missing)
661 available_cols = ", ".join(sorted(available))
662 raise ValueError(
663 f"Column(s) {missing_cols} do not exist for this HubSpot table. Available columns: {available_cols}."
664 )
666 def _get_fetch_columns(
667 self,
668 targets: List[str],
669 normalized_conditions: List[List[Any]],
670 order_by: List[SortColumn],
671 use_search: bool,
672 ) -> List[str]:
673 if targets:
674 base_columns = list(targets)
675 else:
676 base_columns = list(self.get_columns())
678 if use_search:
679 return list(dict.fromkeys(base_columns))
681 extra_columns = []
682 for condition in normalized_conditions:
683 if len(condition) >= 2:
684 extra_columns.append(condition[1])
685 for sort in order_by or []:
686 extra_columns.append(to_internal_property(sort.column))
688 return list(dict.fromkeys(base_columns + extra_columns))
690 def _object_to_dict(self, obj: Any, columns: List[str]) -> Dict[str, Any]:
691 properties = getattr(obj, "properties", {}) or {}
692 row = {}
693 for col in columns:
694 if col == "id":
695 row["id"] = getattr(obj, "id", None)
696 continue
697 row[col] = properties.get(to_hubspot_property(col))
698 return row
701class CompaniesTable(HubSpotAPIResource):
702 """Hubspot Companies table."""
704 SEARCHABLE_COLUMNS = {
705 "name",
706 "domain",
707 "industry",
708 "city",
709 "state",
710 "id",
711 "website",
712 "address",
713 "zip",
714 "numberofemployees",
715 "annualrevenue",
716 "lifecyclestage",
717 "current_erp",
718 "current_erp_version",
719 "current_web_platform",
720 "accounting_software",
721 "credit_card_processor",
722 "data_integration_platform",
723 "marketing_platform",
724 "pos_software",
725 "shipping_software",
726 "tax_platform",
727 "partner",
728 "partner_type",
729 "partnership_status",
730 "partner_payout_ytd",
731 "partnership_commission",
732 "total_customer_value",
733 "total_revenue",
734 "lastmodifieddate",
735 }
737 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
738 row_count = None
739 try:
740 self.handler.connect()
741 row_count = self.handler._estimate_table_rows("companies")
742 except Exception as e:
743 logger.warning(f"Could not estimate HubSpot companies row count: {e}")
745 return {
746 "TABLE_NAME": "companies",
747 "TABLE_TYPE": "BASE TABLE",
748 "TABLE_DESCRIPTION": self.handler._get_table_description("companies"),
749 "ROW_COUNT": row_count,
750 }
752 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
753 return self.handler._get_default_meta_columns("companies")
755 def list(
756 self,
757 conditions: List[FilterCondition] = None,
758 limit: int = None,
759 sort: List[SortColumn] = None,
760 targets: List[str] = None,
761 search_filters: Optional[List[Dict[str, Any]]] = None,
762 search_sorts: Optional[List[Dict[str, Any]]] = None,
763 allow_search: bool = True,
764 ) -> pd.DataFrame:
765 companies_df = pd.json_normalize(
766 self.get_companies(
767 limit=limit,
768 where_conditions=conditions,
769 properties=targets,
770 search_filters=search_filters,
771 search_sorts=search_sorts,
772 allow_search=allow_search,
773 )
774 )
775 if companies_df.empty:
776 companies_df = pd.DataFrame(columns=targets or self._get_default_company_columns())
777 return companies_df
779 def add(self, company_data: List[dict]):
780 self.create_companies(company_data)
782 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
783 normalized_conditions = _normalize_filter_conditions(conditions)
784 companies_df = pd.json_normalize(self.get_companies(limit=200, where_conditions=normalized_conditions))
786 if companies_df.empty:
787 raise ValueError("No companies retrieved from HubSpot to evaluate update conditions.")
789 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
790 update_query_executor = UPDATEQueryExecutor(companies_df, executor_conditions)
791 filtered_df = update_query_executor.execute_query()
793 if filtered_df.empty:
794 raise ValueError(f"No companies found matching WHERE conditions: {conditions}.")
796 company_ids = filtered_df["id"].astype(str).tolist()
797 logger.info(f"Updating {len(company_ids)} compan(ies) matching WHERE conditions")
798 self.update_companies(company_ids, values)
800 def remove(self, conditions: List[FilterCondition]) -> None:
801 normalized_conditions = _normalize_filter_conditions(conditions)
802 companies_df = pd.json_normalize(self.get_companies(limit=200, where_conditions=normalized_conditions))
804 if companies_df.empty:
805 raise ValueError("No companies retrieved from HubSpot to evaluate delete conditions.")
807 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
808 delete_query_executor = DELETEQueryExecutor(companies_df, executor_conditions)
809 filtered_df = delete_query_executor.execute_query()
811 if filtered_df.empty:
812 raise ValueError(f"No companies found matching WHERE conditions: {conditions}.")
814 company_ids = filtered_df["id"].astype(str).tolist()
815 logger.info(f"Deleting {len(company_ids)} compan(ies) matching WHERE conditions")
816 self.delete_companies(company_ids)
818 def get_columns(self) -> List[Text]:
819 return self._get_default_company_columns()
821 @staticmethod
822 def _get_default_company_columns() -> List[str]:
823 return [
824 "id",
825 "name",
826 "city",
827 "phone",
828 "state",
829 "domain",
830 "industry",
831 "website",
832 "address",
833 "zip",
834 "numberofemployees",
835 "annualrevenue",
836 "lifecyclestage",
837 "current_erp",
838 "current_erp_version",
839 "current_web_platform",
840 "accounting_software",
841 "credit_card_processor",
842 "data_integration_platform",
843 "marketing_platform",
844 "pos_software",
845 "shipping_software",
846 "tax_platform",
847 "partner",
848 "partner_type",
849 "partnership_status",
850 "partner_payout_ytd",
851 "partnership_commission",
852 "total_customer_value",
853 "total_revenue",
854 "createdate",
855 "lastmodifieddate",
856 ]
858 def get_companies(
859 self,
860 limit: Optional[int] = None,
861 where_conditions: Optional[List] = None,
862 properties: Optional[List[str]] = None,
863 search_filters: Optional[List[Dict[str, Any]]] = None,
864 search_sorts: Optional[List[Dict[str, Any]]] = None,
865 allow_search: bool = True,
866 **kwargs,
867 ) -> List[Dict]:
868 normalized_conditions = _normalize_filter_conditions(where_conditions)
869 hubspot = self.handler.connect()
871 requested_properties = properties or []
872 default_properties = self._get_default_company_columns()
873 columns = requested_properties or default_properties
874 hubspot_properties = _build_hubspot_properties(columns)
876 api_kwargs = {**kwargs, "properties": hubspot_properties}
877 if limit is not None:
878 api_kwargs["limit"] = limit
879 else:
880 api_kwargs.pop("limit", None)
882 if allow_search and (search_filters or search_sorts or normalized_conditions):
883 filters = search_filters
884 if filters is None and normalized_conditions:
885 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
886 if filters is not None or search_sorts is not None:
887 search_results = self._search_companies_by_conditions(
888 hubspot, filters, hubspot_properties, limit, search_sorts, columns
889 )
890 logger.info(f"Retrieved {len(search_results)} companies from HubSpot via search API")
891 return search_results
893 companies = hubspot.crm.companies.get_all(**api_kwargs)
894 companies_dict = []
895 for company in companies:
896 try:
897 companies_dict.append(self._company_to_dict(company, columns))
898 except Exception as e:
899 logger.warning(f"Error processing company {getattr(company, 'id', 'unknown')}: {str(e)}")
900 continue
902 logger.info(f"Retrieved {len(companies_dict)} companies from HubSpot")
903 return companies_dict
905 def _search_companies_by_conditions(
906 self,
907 hubspot: HubSpot,
908 filters: Optional[List[Dict[str, Any]]],
909 properties: List[str],
910 limit: Optional[int],
911 sorts: Optional[List[Dict[str, Any]]],
912 columns: List[str],
913 ) -> List[Dict[str, Any]]:
914 return _execute_hubspot_search(
915 hubspot.crm.companies.search_api,
916 filters or [],
917 properties,
918 limit,
919 lambda obj: self._company_to_dict(obj, columns),
920 sorts=sorts,
921 )
923 def _company_to_dict(self, company: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
924 columns = columns or self._get_default_company_columns()
925 return self._object_to_dict(company, columns)
927 def create_companies(self, companies_data: List[Dict[Text, Any]]) -> None:
928 if not companies_data:
929 raise ValueError("No company data provided for creation")
931 logger.info(f"Attempting to create {len(companies_data)} compan(ies)")
932 hubspot = self.handler.connect()
933 companies_to_create = [HubSpotObjectInputCreate(properties=company) for company in companies_data]
934 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=companies_to_create)
936 try:
937 created_companies = hubspot.crm.companies.batch_api.create(
938 batch_input_simple_public_object_batch_input_for_create=batch_input
939 )
940 if not created_companies or not hasattr(created_companies, "results") or not created_companies.results:
941 raise Exception("Company creation returned no results")
942 created_ids = [c.id for c in created_companies.results]
943 logger.info(f"Successfully created {len(created_ids)} compan(ies) with IDs: {created_ids}")
944 except Exception as e:
945 logger.error(f"Companies creation failed: {str(e)}")
946 raise Exception(f"Companies creation failed {e}")
948 def update_companies(self, company_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
949 hubspot = self.handler.connect()
950 companies_to_update = [HubSpotObjectBatchInput(id=cid, properties=values_to_update) for cid in company_ids]
951 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=companies_to_update)
952 try:
953 updated = hubspot.crm.companies.batch_api.update(batch_input_simple_public_object_batch_input=batch_input)
954 logger.info(f"Companies with ID {[c.id for c in updated.results]} updated")
955 except Exception as e:
956 raise Exception(f"Companies update failed {e}")
958 def delete_companies(self, company_ids: List[Text]) -> None:
959 hubspot = self.handler.connect()
960 companies_to_delete = [HubSpotObjectId(id=cid) for cid in company_ids]
961 batch_input = BatchInputSimplePublicObjectId(inputs=companies_to_delete)
962 try:
963 hubspot.crm.companies.batch_api.archive(batch_input_simple_public_object_id=batch_input)
964 logger.info("Companies deleted")
965 except Exception as e:
966 raise Exception(f"Companies deletion failed {e}")
969class ContactsTable(HubSpotAPIResource):
970 """Hubspot Contacts table."""
972 SEARCHABLE_COLUMNS = {
973 "email",
974 "id",
975 "firstname",
976 "lastname",
977 "phone",
978 "mobilephone",
979 "jobtitle",
980 "company",
981 "city",
982 "website",
983 "lifecyclestage",
984 "hs_lead_status",
985 "hubspot_owner_id",
986 "dc_contact",
987 "current_ecommerce_platform",
988 "departments",
989 "demo__requested",
990 "linkedin_url",
991 "referral_name",
992 "referral_company_name",
993 "notes_last_contacted",
994 "notes_last_updated",
995 "notes_next_activity_date",
996 "num_contacted_notes",
997 "hs_sales_email_last_clicked",
998 "hs_sales_email_last_opened",
999 "lastmodifieddate",
1000 }
1002 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
1003 row_count = None
1004 try:
1005 self.handler.connect()
1006 row_count = self.handler._estimate_table_rows("contacts")
1007 except Exception as e:
1008 logger.warning(f"Could not estimate HubSpot contacts row count: {e}")
1010 return {
1011 "TABLE_NAME": "contacts",
1012 "TABLE_TYPE": "BASE TABLE",
1013 "TABLE_DESCRIPTION": self.handler._get_table_description("contacts"),
1014 "ROW_COUNT": row_count,
1015 }
1017 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
1018 return self.handler._get_default_meta_columns("contacts")
1020 def list(
1021 self,
1022 conditions: List[FilterCondition] = None,
1023 limit: int = None,
1024 sort: List[SortColumn] = None,
1025 targets: List[str] = None,
1026 search_filters: Optional[List[Dict[str, Any]]] = None,
1027 search_sorts: Optional[List[Dict[str, Any]]] = None,
1028 allow_search: bool = True,
1029 ) -> pd.DataFrame:
1030 contacts_df = pd.json_normalize(
1031 self.get_contacts(
1032 limit=limit,
1033 where_conditions=conditions,
1034 properties=targets,
1035 search_filters=search_filters,
1036 search_sorts=search_sorts,
1037 allow_search=allow_search,
1038 )
1039 )
1040 if contacts_df.empty:
1041 contacts_df = pd.DataFrame(columns=targets or self._get_default_contact_columns())
1042 else:
1043 if "id" in contacts_df.columns:
1044 contacts_df["id"] = pd.to_numeric(contacts_df["id"], errors="coerce")
1045 return contacts_df
1047 def add(self, contact_data: List[dict]):
1048 self.create_contacts(contact_data)
1050 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
1051 where_conditions = _normalize_filter_conditions(conditions)
1052 contacts_df = pd.json_normalize(self.get_contacts(limit=200, where_conditions=where_conditions))
1054 if contacts_df.empty:
1055 raise ValueError("No contacts retrieved from HubSpot to evaluate update conditions.")
1057 executor_conditions = _normalize_conditions_for_executor(where_conditions)
1058 update_query_executor = UPDATEQueryExecutor(contacts_df, executor_conditions)
1059 filtered_df = update_query_executor.execute_query()
1061 if filtered_df.empty:
1062 raise ValueError(f"No contacts found matching WHERE conditions: {conditions}.")
1064 contact_ids = filtered_df["id"].astype(str).tolist()
1065 logger.info(f"Updating {len(contact_ids)} contact(s) matching WHERE conditions")
1066 self.update_contacts(contact_ids, values)
1068 def remove(self, conditions: List[FilterCondition]) -> None:
1069 where_conditions = _normalize_filter_conditions(conditions)
1070 contacts_df = pd.json_normalize(self.get_contacts(limit=200, where_conditions=where_conditions))
1072 if contacts_df.empty:
1073 raise ValueError("No contacts retrieved from HubSpot to evaluate delete conditions.")
1075 executor_conditions = _normalize_conditions_for_executor(where_conditions)
1076 delete_query_executor = DELETEQueryExecutor(contacts_df, executor_conditions)
1077 filtered_df = delete_query_executor.execute_query()
1079 if filtered_df.empty:
1080 raise ValueError(f"No contacts found matching WHERE conditions: {conditions}.")
1082 contact_ids = filtered_df["id"].astype(str).tolist()
1083 logger.info(f"Deleting {len(contact_ids)} contact(s) matching WHERE conditions")
1084 self.delete_contacts(contact_ids)
1086 def get_columns(self) -> List[Text]:
1087 return self._get_default_contact_columns()
1089 @staticmethod
1090 def _get_default_contact_columns() -> List[str]:
1091 return [
1092 "id",
1093 "email",
1094 "firstname",
1095 "lastname",
1096 "phone",
1097 "mobilephone",
1098 "jobtitle",
1099 "company",
1100 "city",
1101 "website",
1102 "lifecyclestage",
1103 "hs_lead_status",
1104 "hubspot_owner_id",
1105 "dc_contact",
1106 "current_ecommerce_platform",
1107 "departments",
1108 "demo__requested",
1109 "linkedin_url",
1110 "referral_name",
1111 "referral_company_name",
1112 "notes_last_contacted",
1113 "notes_last_updated",
1114 "notes_next_activity_date",
1115 "num_contacted_notes",
1116 "hs_sales_email_last_clicked",
1117 "hs_sales_email_last_opened",
1118 "createdate",
1119 "lastmodifieddate",
1120 ]
1122 def get_contacts(
1123 self,
1124 limit: Optional[int] = None,
1125 where_conditions: Optional[List] = None,
1126 properties: Optional[List[str]] = None,
1127 search_filters: Optional[List[Dict[str, Any]]] = None,
1128 search_sorts: Optional[List[Dict[str, Any]]] = None,
1129 allow_search: bool = True,
1130 **kwargs,
1131 ) -> List[Dict]:
1132 normalized_conditions = _normalize_filter_conditions(where_conditions)
1133 hubspot = self.handler.connect()
1134 requested_properties = properties or []
1135 default_properties = self._get_default_contact_columns()
1136 columns = requested_properties or default_properties
1137 hubspot_properties = _build_hubspot_properties(columns)
1139 api_kwargs = {**kwargs, "properties": hubspot_properties}
1140 if limit is not None:
1141 api_kwargs["limit"] = limit
1142 else:
1143 api_kwargs.pop("limit", None)
1145 if allow_search and (search_filters or search_sorts or normalized_conditions):
1146 filters = search_filters
1147 if filters is None and normalized_conditions:
1148 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
1149 if filters is not None or search_sorts is not None:
1150 search_results = self._search_contacts_by_conditions(
1151 hubspot, filters, hubspot_properties, limit, search_sorts, columns
1152 )
1153 logger.info(f"Retrieved {len(search_results)} contacts from HubSpot via search API")
1154 return search_results
1156 contacts = hubspot.crm.contacts.get_all(**api_kwargs)
1157 contacts_dict = []
1158 try:
1159 for contact in contacts:
1160 contacts_dict.append(self._contact_to_dict(contact, columns))
1161 if limit is not None and len(contacts_dict) >= limit:
1162 break
1163 except Exception as e:
1164 logger.error(f"Failed to iterate HubSpot contacts: {str(e)}")
1165 raise
1167 logger.info(f"Retrieved {len(contacts_dict)} contacts from HubSpot")
1168 return contacts_dict
1170 def _search_contacts_by_conditions(
1171 self,
1172 hubspot: HubSpot,
1173 filters: Optional[List[Dict[str, Any]]],
1174 properties: List[str],
1175 limit: Optional[int],
1176 sorts: Optional[List[Dict[str, Any]]],
1177 columns: List[str],
1178 ) -> List[Dict[str, Any]]:
1179 return _execute_hubspot_search(
1180 hubspot.crm.contacts.search_api,
1181 filters or [],
1182 properties,
1183 limit,
1184 lambda obj: self._contact_to_dict(obj, columns),
1185 sorts=sorts,
1186 )
1188 def _contact_to_dict(self, contact: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
1189 columns = columns or self._get_default_contact_columns()
1190 try:
1191 return self._object_to_dict(contact, columns)
1192 except Exception as e:
1193 logger.warning(f"Error processing contact {getattr(contact, 'id', 'unknown')}: {str(e)}")
1194 return {
1195 "id": getattr(contact, "id", None),
1196 **{col: None for col in columns if col != "id"},
1197 }
1199 def create_contacts(self, contacts_data: List[Dict[Text, Any]]) -> None:
1200 if not contacts_data:
1201 raise ValueError("No contact data provided for creation")
1203 logger.info(f"Attempting to create {len(contacts_data)} contact(s)")
1204 hubspot = self.handler.connect()
1205 contacts_to_create = [HubSpotObjectInputCreate(properties=contact) for contact in contacts_data]
1206 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=contacts_to_create)
1208 try:
1209 created_contacts = hubspot.crm.contacts.batch_api.create(
1210 batch_input_simple_public_object_batch_input_for_create=batch_input
1211 )
1212 if not created_contacts or not hasattr(created_contacts, "results") or not created_contacts.results:
1213 raise Exception("Contact creation returned no results")
1214 created_ids = [c.id for c in created_contacts.results]
1215 logger.info(f"Successfully created {len(created_ids)} contact(s) with IDs: {created_ids}")
1216 except Exception as e:
1217 logger.error(f"Contacts creation failed: {str(e)}")
1218 raise Exception(f"Contacts creation failed {e}")
1220 def update_contacts(self, contact_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
1221 hubspot = self.handler.connect()
1222 contacts_to_update = [HubSpotObjectBatchInput(id=cid, properties=values_to_update) for cid in contact_ids]
1223 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=contacts_to_update)
1224 try:
1225 updated = hubspot.crm.contacts.batch_api.update(batch_input_simple_public_object_batch_input=batch_input)
1226 logger.info(f"Contacts with ID {[c.id for c in updated.results]} updated")
1227 except Exception as e:
1228 raise Exception(f"Contacts update failed {e}")
1230 def delete_contacts(self, contact_ids: List[Text]) -> None:
1231 hubspot = self.handler.connect()
1232 contacts_to_delete = [HubSpotObjectId(id=cid) for cid in contact_ids]
1233 batch_input = BatchInputSimplePublicObjectId(inputs=contacts_to_delete)
1234 try:
1235 hubspot.crm.contacts.batch_api.archive(batch_input_simple_public_object_id=batch_input)
1236 logger.info("Contacts deleted")
1237 except Exception as e:
1238 raise Exception(f"Contacts deletion failed {e}")
1241class DealsTable(HubSpotAPIResource):
1242 """Hubspot Deals table."""
1244 SEARCHABLE_COLUMNS = {
1245 "dealname",
1246 "amount",
1247 "dealstage",
1248 "pipeline",
1249 "closedate",
1250 "hubspot_owner_id",
1251 "closed_won_reason",
1252 "closed_lost_reason",
1253 "lead_attribution",
1254 "services_requested",
1255 "platform",
1256 "referral_partner",
1257 "referral_commission_amount",
1258 "tech_partners_involved",
1259 "sales_tier",
1260 "commission_status",
1261 "id",
1262 "lastmodifieddate",
1263 }
1265 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
1266 row_count = None
1267 try:
1268 self.handler.connect()
1269 row_count = self.handler._estimate_table_rows("deals")
1270 except Exception as e:
1271 logger.warning(f"Could not estimate HubSpot deals row count: {e}")
1273 return {
1274 "TABLE_NAME": "deals",
1275 "TABLE_TYPE": "BASE TABLE",
1276 "TABLE_DESCRIPTION": self.handler._get_table_description("deals"),
1277 "ROW_COUNT": row_count,
1278 }
1280 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
1281 return self.handler._get_default_meta_columns("deals")
1283 def list(
1284 self,
1285 conditions: List[FilterCondition] = None,
1286 limit: int = None,
1287 sort: List[SortColumn] = None,
1288 targets: List[str] = None,
1289 search_filters: Optional[List[Dict[str, Any]]] = None,
1290 search_sorts: Optional[List[Dict[str, Any]]] = None,
1291 allow_search: bool = True,
1292 ) -> pd.DataFrame:
1293 deals_df = pd.json_normalize(
1294 self.get_deals(
1295 limit=limit,
1296 where_conditions=conditions,
1297 properties=targets,
1298 search_filters=search_filters,
1299 search_sorts=search_sorts,
1300 allow_search=allow_search,
1301 )
1302 )
1303 if deals_df.empty:
1304 deals_df = pd.DataFrame(columns=targets or self._get_default_deal_columns())
1305 else:
1306 deals_df = self._cast_deal_columns(deals_df)
1307 return deals_df
1309 def add(self, deal_data: List[dict]):
1310 self.create_deals(deal_data)
1312 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
1313 where_conditions = _normalize_filter_conditions(conditions)
1314 deals_df = pd.json_normalize(self.get_deals(limit=200, where_conditions=where_conditions))
1316 if deals_df.empty:
1317 raise ValueError("No deals retrieved from HubSpot to evaluate update conditions.")
1319 executor_conditions = _normalize_conditions_for_executor(where_conditions)
1320 update_query_executor = UPDATEQueryExecutor(deals_df, executor_conditions)
1321 filtered_df = update_query_executor.execute_query()
1323 if filtered_df.empty:
1324 raise ValueError(f"No deals found matching WHERE conditions: {conditions}.")
1326 deal_ids = filtered_df["id"].astype(str).tolist()
1327 logger.info(f"Updating {len(deal_ids)} deal(s) matching WHERE conditions")
1328 self.update_deals(deal_ids, values)
1330 def remove(self, conditions: List[FilterCondition]) -> None:
1331 where_conditions = _normalize_filter_conditions(conditions)
1332 deals_df = pd.json_normalize(self.get_deals(limit=200, where_conditions=where_conditions))
1334 if deals_df.empty:
1335 raise ValueError("No deals retrieved from HubSpot to evaluate delete conditions.")
1337 executor_conditions = _normalize_conditions_for_executor(where_conditions)
1338 delete_query_executor = DELETEQueryExecutor(deals_df, executor_conditions)
1339 filtered_df = delete_query_executor.execute_query()
1341 if filtered_df.empty:
1342 raise ValueError(f"No deals found matching WHERE conditions: {conditions}.")
1344 deal_ids = filtered_df["id"].astype(str).tolist()
1345 logger.info(f"Deleting {len(deal_ids)} deal(s) matching WHERE conditions")
1346 self.delete_deals(deal_ids)
1348 def get_columns(self) -> List[Text]:
1349 return self._get_default_deal_columns()
1351 @staticmethod
1352 def _get_default_deal_columns() -> List[str]:
1353 return [
1354 "id",
1355 "dealname",
1356 "amount",
1357 "pipeline",
1358 "closedate",
1359 "dealstage",
1360 "hubspot_owner_id",
1361 "closed_won_reason",
1362 "closed_lost_reason",
1363 "lead_attribution",
1364 "services_requested",
1365 "platform",
1366 "referral_partner",
1367 "referral_commission_amount",
1368 "tech_partners_involved",
1369 "sales_tier",
1370 "commission_status",
1371 "createdate",
1372 "lastmodifieddate",
1373 ]
1375 @staticmethod
1376 def _cast_deal_columns(deals_df: pd.DataFrame) -> pd.DataFrame:
1377 numeric_columns = ["amount"]
1378 datetime_columns = ["closedate", "createdate", "lastmodifieddate"]
1379 for column in numeric_columns:
1380 if column in deals_df.columns:
1381 deals_df[column] = pd.to_numeric(deals_df[column], errors="coerce")
1382 for column in datetime_columns:
1383 if column in deals_df.columns:
1384 deals_df[column] = pd.to_datetime(deals_df[column], errors="coerce")
1385 return deals_df
1387 def get_deals(
1388 self,
1389 limit: Optional[int] = None,
1390 where_conditions: Optional[List] = None,
1391 properties: Optional[List[str]] = None,
1392 search_filters: Optional[List[Dict[str, Any]]] = None,
1393 search_sorts: Optional[List[Dict[str, Any]]] = None,
1394 allow_search: bool = True,
1395 **kwargs,
1396 ) -> List[Dict]:
1397 normalized_conditions = _normalize_filter_conditions(where_conditions)
1398 hubspot = self.handler.connect()
1399 requested_properties = properties or []
1400 default_properties = self._get_default_deal_columns()
1401 columns = requested_properties or default_properties
1402 hubspot_properties = _build_hubspot_properties(columns)
1404 api_kwargs = {**kwargs, "properties": hubspot_properties}
1405 if limit is not None:
1406 api_kwargs["limit"] = limit
1407 else:
1408 api_kwargs.pop("limit", None)
1410 if allow_search and (search_filters or search_sorts or normalized_conditions):
1411 filters = search_filters
1412 if filters is None and normalized_conditions:
1413 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
1414 if filters is not None or search_sorts is not None:
1415 search_results = self._search_deals_by_conditions(
1416 hubspot, filters, hubspot_properties, limit, search_sorts, columns
1417 )
1418 logger.info(f"Retrieved {len(search_results)} deals from HubSpot via search API")
1419 return search_results
1421 deals = hubspot.crm.deals.get_all(**api_kwargs)
1422 deals_dict = []
1423 for deal in deals:
1424 try:
1425 deals_dict.append(self._deal_to_dict(deal, columns))
1426 except Exception as e:
1427 logger.error(f"Error processing deal {getattr(deal, 'id', 'unknown')}: {str(e)}")
1428 raise ValueError(f"Failed to process deal {getattr(deal, 'id', 'unknown')}.") from e
1430 logger.info(f"Retrieved {len(deals_dict)} deals from HubSpot")
1431 return deals_dict
1433 def _search_deals_by_conditions(
1434 self,
1435 hubspot: HubSpot,
1436 filters: Optional[List[Dict[str, Any]]],
1437 properties: List[str],
1438 limit: Optional[int],
1439 sorts: Optional[List[Dict[str, Any]]],
1440 columns: List[str],
1441 ) -> List[Dict[str, Any]]:
1442 return _execute_hubspot_search(
1443 hubspot.crm.deals.search_api,
1444 filters or [],
1445 properties,
1446 limit,
1447 lambda obj: self._deal_to_dict(obj, columns),
1448 sorts=sorts,
1449 )
1451 def _deal_to_dict(self, deal: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
1452 columns = columns or self._get_default_deal_columns()
1453 return self._object_to_dict(deal, columns)
1455 def create_deals(self, deals_data: List[Dict[Text, Any]]) -> None:
1456 if not deals_data:
1457 raise ValueError("No deal data provided for creation")
1459 logger.info(f"Attempting to create {len(deals_data)} deal(s)")
1460 hubspot = self.handler.connect()
1461 deals_to_create = [HubSpotObjectInputCreate(properties=deal) for deal in deals_data]
1462 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=deals_to_create)
1464 try:
1465 created_deals = hubspot.crm.deals.batch_api.create(
1466 batch_input_simple_public_object_batch_input_for_create=batch_input
1467 )
1468 if not created_deals or not hasattr(created_deals, "results") or not created_deals.results:
1469 raise Exception("Deal creation returned no results")
1470 created_ids = [d.id for d in created_deals.results]
1471 logger.info(f"Successfully created {len(created_ids)} deal(s) with IDs: {created_ids}")
1472 except Exception as e:
1473 logger.error(f"Deals creation failed: {str(e)}")
1474 raise Exception(f"Deals creation failed {e}")
1476 def update_deals(self, deal_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
1477 hubspot = self.handler.connect()
1478 deals_to_update = [HubSpotObjectBatchInput(id=did, properties=values_to_update) for did in deal_ids]
1479 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=deals_to_update)
1480 try:
1481 updated = hubspot.crm.deals.batch_api.update(batch_input_simple_public_object_batch_input=batch_input)
1482 logger.info(f"Deals with ID {[d.id for d in updated.results]} updated")
1483 except Exception as e:
1484 raise Exception(f"Deals update failed {e}")
1486 def delete_deals(self, deal_ids: List[Text]) -> None:
1487 hubspot = self.handler.connect()
1488 deals_to_delete = [HubSpotObjectId(id=did) for did in deal_ids]
1489 batch_input = BatchInputSimplePublicObjectId(inputs=deals_to_delete)
1490 try:
1491 hubspot.crm.deals.batch_api.archive(batch_input_simple_public_object_id=batch_input)
1492 logger.info("Deals deleted")
1493 except Exception as e:
1494 raise Exception(f"Deals deletion failed {e}")
1497class TicketsTable(HubSpotAPIResource):
1498 """HubSpot Tickets table for support ticket management."""
1500 SEARCHABLE_COLUMNS = {"subject", "hs_pipeline", "hs_pipeline_stage", "hs_ticket_priority", "id"}
1502 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
1503 row_count = None
1504 try:
1505 self.handler.connect()
1506 row_count = self.handler._estimate_table_rows("tickets")
1507 except Exception as e:
1508 logger.warning(f"Could not estimate HubSpot tickets row count: {e}")
1510 return {
1511 "TABLE_NAME": "tickets",
1512 "TABLE_TYPE": "BASE TABLE",
1513 "TABLE_DESCRIPTION": "HubSpot tickets data including subject, status, priority and pipeline information",
1514 "ROW_COUNT": row_count,
1515 }
1517 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
1518 return self.handler._get_default_meta_columns("tickets")
1520 def list(
1521 self,
1522 conditions: List[FilterCondition] = None,
1523 limit: int = None,
1524 sort: List[SortColumn] = None,
1525 targets: List[str] = None,
1526 search_filters: Optional[List[Dict[str, Any]]] = None,
1527 search_sorts: Optional[List[Dict[str, Any]]] = None,
1528 allow_search: bool = True,
1529 ) -> pd.DataFrame:
1530 tickets_df = pd.json_normalize(
1531 self.get_tickets(
1532 limit=limit,
1533 where_conditions=conditions,
1534 properties=targets,
1535 search_filters=search_filters,
1536 search_sorts=search_sorts,
1537 allow_search=allow_search,
1538 )
1539 )
1540 if tickets_df.empty:
1541 tickets_df = pd.DataFrame(columns=targets or self._get_default_ticket_columns())
1542 return tickets_df
1544 def add(self, ticket_data: List[dict]):
1545 self.create_tickets(ticket_data)
1547 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
1548 normalized_conditions = _normalize_filter_conditions(conditions)
1549 tickets_df = pd.json_normalize(self.get_tickets(limit=200, where_conditions=normalized_conditions))
1551 if tickets_df.empty:
1552 raise ValueError("No tickets retrieved from HubSpot to evaluate update conditions.")
1554 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
1555 update_query_executor = UPDATEQueryExecutor(tickets_df, executor_conditions)
1556 filtered_df = update_query_executor.execute_query()
1558 if filtered_df.empty:
1559 raise ValueError(f"No tickets found matching WHERE conditions: {conditions}.")
1561 ticket_ids = filtered_df["id"].astype(str).tolist()
1562 logger.info(f"Updating {len(ticket_ids)} ticket(s) matching WHERE conditions")
1563 self.update_tickets(ticket_ids, values)
1565 def remove(self, conditions: List[FilterCondition]) -> None:
1566 normalized_conditions = _normalize_filter_conditions(conditions)
1567 tickets_df = pd.json_normalize(self.get_tickets(limit=200, where_conditions=normalized_conditions))
1569 if tickets_df.empty:
1570 raise ValueError("No tickets retrieved from HubSpot to evaluate delete conditions.")
1572 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
1573 delete_query_executor = DELETEQueryExecutor(tickets_df, executor_conditions)
1574 filtered_df = delete_query_executor.execute_query()
1576 if filtered_df.empty:
1577 raise ValueError(f"No tickets found matching WHERE conditions: {conditions}.")
1579 ticket_ids = filtered_df["id"].astype(str).tolist()
1580 logger.info(f"Deleting {len(ticket_ids)} ticket(s) matching WHERE conditions")
1581 self.delete_tickets(ticket_ids)
1583 def get_columns(self) -> List[Text]:
1584 return self._get_default_ticket_columns()
1586 @staticmethod
1587 def _get_default_ticket_columns() -> List[str]:
1588 return [
1589 "id",
1590 "subject",
1591 "content",
1592 "hs_pipeline",
1593 "hs_pipeline_stage",
1594 "hs_ticket_priority",
1595 "hs_ticket_category",
1596 "hubspot_owner_id",
1597 "createdate",
1598 "lastmodifieddate",
1599 ]
1601 def get_tickets(
1602 self,
1603 limit: Optional[int] = None,
1604 where_conditions: Optional[List] = None,
1605 properties: Optional[List[str]] = None,
1606 search_filters: Optional[List[Dict[str, Any]]] = None,
1607 search_sorts: Optional[List[Dict[str, Any]]] = None,
1608 allow_search: bool = True,
1609 **kwargs,
1610 ) -> List[Dict]:
1611 normalized_conditions = _normalize_filter_conditions(where_conditions)
1612 hubspot = self.handler.connect()
1614 requested_properties = properties or []
1615 default_properties = self._get_default_ticket_columns()
1616 columns = requested_properties or default_properties
1617 hubspot_properties = _build_hubspot_properties(columns)
1619 api_kwargs = {**kwargs, "properties": hubspot_properties}
1620 if limit is not None:
1621 api_kwargs["limit"] = limit
1622 else:
1623 api_kwargs.pop("limit", None)
1625 if allow_search and (search_filters or search_sorts or normalized_conditions):
1626 filters = search_filters
1627 if filters is None and normalized_conditions:
1628 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
1629 if filters is not None or search_sorts is not None:
1630 search_results = self._search_tickets_by_conditions(
1631 hubspot, filters, hubspot_properties, limit, search_sorts, columns
1632 )
1633 logger.info(f"Retrieved {len(search_results)} tickets from HubSpot via search API")
1634 return search_results
1636 tickets = hubspot.crm.tickets.get_all(**api_kwargs)
1637 tickets_dict = []
1638 for ticket in tickets:
1639 try:
1640 tickets_dict.append(self._ticket_to_dict(ticket, columns))
1641 except Exception as e:
1642 logger.warning(f"Error processing ticket {getattr(ticket, 'id', 'unknown')}: {str(e)}")
1643 continue
1645 logger.info(f"Retrieved {len(tickets_dict)} tickets from HubSpot")
1646 return tickets_dict
1648 def _search_tickets_by_conditions(
1649 self,
1650 hubspot: HubSpot,
1651 filters: Optional[List[Dict[str, Any]]],
1652 properties: List[str],
1653 limit: Optional[int],
1654 sorts: Optional[List[Dict[str, Any]]],
1655 columns: List[str],
1656 ) -> List[Dict[str, Any]]:
1657 return _execute_hubspot_search(
1658 hubspot.crm.tickets.search_api,
1659 filters or [],
1660 properties,
1661 limit,
1662 lambda obj: self._ticket_to_dict(obj, columns),
1663 sorts=sorts,
1664 )
1666 def _ticket_to_dict(self, ticket: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
1667 columns = columns or self._get_default_ticket_columns()
1668 return self._object_to_dict(ticket, columns)
1670 def create_tickets(self, tickets_data: List[Dict[Text, Any]]) -> None:
1671 if not tickets_data:
1672 raise ValueError("No ticket data provided for creation")
1674 logger.info(f"Attempting to create {len(tickets_data)} ticket(s)")
1675 hubspot = self.handler.connect()
1676 tickets_to_create = [HubSpotObjectInputCreate(properties=ticket) for ticket in tickets_data]
1677 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=tickets_to_create)
1679 try:
1680 created_tickets = hubspot.crm.tickets.batch_api.create(
1681 batch_input_simple_public_object_batch_input_for_create=batch_input
1682 )
1683 if not created_tickets or not hasattr(created_tickets, "results") or not created_tickets.results:
1684 raise Exception("Ticket creation returned no results")
1685 created_ids = [t.id for t in created_tickets.results]
1686 logger.info(f"Successfully created {len(created_ids)} ticket(s) with IDs: {created_ids}")
1687 except Exception as e:
1688 logger.error(f"Tickets creation failed: {str(e)}")
1689 raise Exception(f"Tickets creation failed {e}")
1691 def update_tickets(self, ticket_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
1692 hubspot = self.handler.connect()
1693 tickets_to_update = [HubSpotObjectBatchInput(id=tid, properties=values_to_update) for tid in ticket_ids]
1694 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=tickets_to_update)
1695 try:
1696 updated = hubspot.crm.tickets.batch_api.update(batch_input_simple_public_object_batch_input=batch_input)
1697 logger.info(f"Tickets with ID {[t.id for t in updated.results]} updated")
1698 except Exception as e:
1699 raise Exception(f"Tickets update failed {e}")
1701 def delete_tickets(self, ticket_ids: List[Text]) -> None:
1702 hubspot = self.handler.connect()
1703 tickets_to_delete = [HubSpotObjectId(id=tid) for tid in ticket_ids]
1704 batch_input = BatchInputSimplePublicObjectId(inputs=tickets_to_delete)
1705 try:
1706 hubspot.crm.tickets.batch_api.archive(batch_input_simple_public_object_id=batch_input)
1707 logger.info("Tickets deleted")
1708 except Exception as e:
1709 raise Exception(f"Tickets deletion failed {e}")
1712class TasksTable(HubSpotAPIResource):
1713 """HubSpot Tasks table for task management and follow-ups."""
1715 SEARCHABLE_COLUMNS = {"hs_task_subject", "hs_task_status", "hs_task_priority", "hs_task_type", "id"}
1717 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
1718 row_count = None
1719 try:
1720 self.handler.connect()
1721 row_count = self.handler._estimate_table_rows("tasks")
1722 except Exception as e:
1723 logger.warning(f"Could not estimate HubSpot tasks row count: {e}")
1725 return {
1726 "TABLE_NAME": "tasks",
1727 "TABLE_TYPE": "BASE TABLE",
1728 "TABLE_DESCRIPTION": "HubSpot tasks data including subject, status, priority and due dates",
1729 "ROW_COUNT": row_count,
1730 }
1732 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
1733 return self.handler._get_default_meta_columns("tasks")
1735 def list(
1736 self,
1737 conditions: List[FilterCondition] = None,
1738 limit: int = None,
1739 sort: List[SortColumn] = None,
1740 targets: List[str] = None,
1741 search_filters: Optional[List[Dict[str, Any]]] = None,
1742 search_sorts: Optional[List[Dict[str, Any]]] = None,
1743 allow_search: bool = True,
1744 ) -> pd.DataFrame:
1745 tasks_df = pd.json_normalize(
1746 self.get_tasks(
1747 limit=limit,
1748 where_conditions=conditions,
1749 properties=targets,
1750 search_filters=search_filters,
1751 search_sorts=search_sorts,
1752 allow_search=allow_search,
1753 )
1754 )
1755 if tasks_df.empty:
1756 tasks_df = pd.DataFrame(columns=targets or self._get_default_task_columns())
1757 return tasks_df
1759 def add(self, task_data: List[dict]):
1760 self.create_tasks(task_data)
1762 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
1763 normalized_conditions = _normalize_filter_conditions(conditions)
1764 tasks_df = pd.json_normalize(self.get_tasks(limit=200, where_conditions=normalized_conditions))
1766 if tasks_df.empty:
1767 raise ValueError("No tasks retrieved from HubSpot to evaluate update conditions.")
1769 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
1770 update_query_executor = UPDATEQueryExecutor(tasks_df, executor_conditions)
1771 filtered_df = update_query_executor.execute_query()
1773 if filtered_df.empty:
1774 raise ValueError(f"No tasks found matching WHERE conditions: {conditions}.")
1776 task_ids = filtered_df["id"].astype(str).tolist()
1777 logger.info(f"Updating {len(task_ids)} task(s) matching WHERE conditions")
1778 self.update_tasks(task_ids, values)
1780 def remove(self, conditions: List[FilterCondition]) -> None:
1781 normalized_conditions = _normalize_filter_conditions(conditions)
1782 tasks_df = pd.json_normalize(self.get_tasks(limit=200, where_conditions=normalized_conditions))
1784 if tasks_df.empty:
1785 raise ValueError("No tasks retrieved from HubSpot to evaluate delete conditions.")
1787 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
1788 delete_query_executor = DELETEQueryExecutor(tasks_df, executor_conditions)
1789 filtered_df = delete_query_executor.execute_query()
1791 if filtered_df.empty:
1792 raise ValueError(f"No tasks found matching WHERE conditions: {conditions}.")
1794 task_ids = filtered_df["id"].astype(str).tolist()
1795 logger.info(f"Deleting {len(task_ids)} task(s) matching WHERE conditions")
1796 self.delete_tasks(task_ids)
1798 def get_columns(self) -> List[Text]:
1799 return self._get_default_task_columns()
1801 @staticmethod
1802 def _get_default_task_columns() -> List[str]:
1803 return [
1804 "id",
1805 "hs_task_subject",
1806 "hs_task_body",
1807 "hs_task_status",
1808 "hs_task_priority",
1809 "hs_task_type",
1810 "hs_timestamp",
1811 "hubspot_owner_id",
1812 "createdate",
1813 "lastmodifieddate",
1814 ]
1816 def get_tasks(
1817 self,
1818 limit: Optional[int] = None,
1819 where_conditions: Optional[List] = None,
1820 properties: Optional[List[str]] = None,
1821 search_filters: Optional[List[Dict[str, Any]]] = None,
1822 search_sorts: Optional[List[Dict[str, Any]]] = None,
1823 allow_search: bool = True,
1824 **kwargs,
1825 ) -> List[Dict]:
1826 normalized_conditions = _normalize_filter_conditions(where_conditions)
1827 hubspot = self.handler.connect()
1829 requested_properties = properties or []
1830 default_properties = self._get_default_task_columns()
1831 columns = requested_properties or default_properties
1832 hubspot_properties = _build_hubspot_properties(columns)
1834 api_kwargs = {**kwargs, "properties": hubspot_properties}
1835 if limit is not None:
1836 api_kwargs["limit"] = limit
1837 else:
1838 api_kwargs.pop("limit", None)
1840 # Tasks use the objects API
1841 if allow_search and (search_filters or search_sorts or normalized_conditions):
1842 filters = search_filters
1843 if filters is None and normalized_conditions:
1844 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
1845 if filters is not None or search_sorts is not None:
1846 search_results = self._search_tasks_by_conditions(
1847 hubspot, filters, hubspot_properties, limit, search_sorts, columns
1848 )
1849 logger.info(f"Retrieved {len(search_results)} tasks from HubSpot via search API")
1850 return search_results
1852 tasks = self.handler._get_objects_all("tasks", **api_kwargs)
1853 tasks_dict = []
1854 for task in tasks:
1855 try:
1856 tasks_dict.append(self._task_to_dict(task, columns))
1857 except Exception as e:
1858 logger.warning(f"Error processing task {getattr(task, 'id', 'unknown')}: {str(e)}")
1859 continue
1861 logger.info(f"Retrieved {len(tasks_dict)} tasks from HubSpot")
1862 return tasks_dict
1864 def _search_tasks_by_conditions(
1865 self,
1866 hubspot: HubSpot,
1867 filters: Optional[List[Dict[str, Any]]],
1868 properties: List[str],
1869 limit: Optional[int],
1870 sorts: Optional[List[Dict[str, Any]]],
1871 columns: List[str],
1872 ) -> List[Dict[str, Any]]:
1873 return _execute_hubspot_search(
1874 hubspot.crm.objects.search_api,
1875 filters or [],
1876 properties,
1877 limit,
1878 lambda obj: self._task_to_dict(obj, columns),
1879 sorts=sorts,
1880 object_type="tasks",
1881 )
1883 def _task_to_dict(self, task: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
1884 columns = columns or self._get_default_task_columns()
1885 return self._object_to_dict(task, columns)
1887 def create_tasks(self, tasks_data: List[Dict[Text, Any]]) -> None:
1888 if not tasks_data:
1889 raise ValueError("No task data provided for creation")
1891 logger.info(f"Attempting to create {len(tasks_data)} task(s)")
1892 hubspot = self.handler.connect()
1893 tasks_to_create = [HubSpotObjectInputCreate(properties=task) for task in tasks_data]
1894 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=tasks_to_create)
1896 try:
1897 created_tasks = hubspot.crm.objects.tasks.batch_api.create(
1898 batch_input_simple_public_object_batch_input_for_create=batch_input
1899 )
1900 if not created_tasks or not hasattr(created_tasks, "results") or not created_tasks.results:
1901 raise Exception("Task creation returned no results")
1902 created_ids = [t.id for t in created_tasks.results]
1903 logger.info(f"Successfully created {len(created_ids)} task(s) with IDs: {created_ids}")
1904 except Exception as e:
1905 logger.error(f"Tasks creation failed: {str(e)}")
1906 raise Exception(f"Tasks creation failed {e}")
1908 def update_tasks(self, task_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
1909 hubspot = self.handler.connect()
1910 tasks_to_update = [HubSpotObjectBatchInput(id=tid, properties=values_to_update) for tid in task_ids]
1911 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=tasks_to_update)
1912 try:
1913 updated = hubspot.crm.objects.tasks.batch_api.update(
1914 batch_input_simple_public_object_batch_input=batch_input
1915 )
1916 logger.info(f"Tasks with ID {[t.id for t in updated.results]} updated")
1917 except Exception as e:
1918 raise Exception(f"Tasks update failed {e}")
1920 def delete_tasks(self, task_ids: List[Text]) -> None:
1921 hubspot = self.handler.connect()
1922 tasks_to_delete = [HubSpotObjectId(id=tid) for tid in task_ids]
1923 batch_input = BatchInputSimplePublicObjectId(inputs=tasks_to_delete)
1924 try:
1925 hubspot.crm.objects.tasks.batch_api.archive(batch_input_simple_public_object_id=batch_input)
1926 logger.info("Tasks deleted")
1927 except Exception as e:
1928 raise Exception(f"Tasks deletion failed {e}")
1931class CallsTable(HubSpotAPIResource):
1932 """HubSpot Calls table for phone/video call logs."""
1934 SEARCHABLE_COLUMNS = {"hs_call_title", "hs_call_direction", "hs_call_disposition", "hs_call_status", "id"}
1936 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
1937 row_count = None
1938 try:
1939 self.handler.connect()
1940 row_count = self.handler._estimate_table_rows("calls")
1941 except Exception as e:
1942 logger.warning(f"Could not estimate HubSpot calls row count: {e}")
1944 return {
1945 "TABLE_NAME": "calls",
1946 "TABLE_TYPE": "BASE TABLE",
1947 "TABLE_DESCRIPTION": "HubSpot call logs including direction, duration, outcome and notes",
1948 "ROW_COUNT": row_count,
1949 }
1951 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
1952 return self.handler._get_default_meta_columns("calls")
1954 def list(
1955 self,
1956 conditions: List[FilterCondition] = None,
1957 limit: int = None,
1958 sort: List[SortColumn] = None,
1959 targets: List[str] = None,
1960 search_filters: Optional[List[Dict[str, Any]]] = None,
1961 search_sorts: Optional[List[Dict[str, Any]]] = None,
1962 allow_search: bool = True,
1963 ) -> pd.DataFrame:
1964 calls_df = pd.json_normalize(
1965 self.get_calls(
1966 limit=limit,
1967 where_conditions=conditions,
1968 properties=targets,
1969 search_filters=search_filters,
1970 search_sorts=search_sorts,
1971 allow_search=allow_search,
1972 )
1973 )
1974 if calls_df.empty:
1975 calls_df = pd.DataFrame(columns=targets or self._get_default_call_columns())
1976 return calls_df
1978 def add(self, call_data: List[dict]):
1979 self.create_calls(call_data)
1981 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
1982 normalized_conditions = _normalize_filter_conditions(conditions)
1983 calls_df = pd.json_normalize(self.get_calls(limit=200, where_conditions=normalized_conditions))
1985 if calls_df.empty:
1986 raise ValueError("No calls retrieved from HubSpot to evaluate update conditions.")
1988 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
1989 update_query_executor = UPDATEQueryExecutor(calls_df, executor_conditions)
1990 filtered_df = update_query_executor.execute_query()
1992 if filtered_df.empty:
1993 raise ValueError(f"No calls found matching WHERE conditions: {conditions}.")
1995 call_ids = filtered_df["id"].astype(str).tolist()
1996 logger.info(f"Updating {len(call_ids)} call(s) matching WHERE conditions")
1997 self.update_calls(call_ids, values)
1999 def remove(self, conditions: List[FilterCondition]) -> None:
2000 normalized_conditions = _normalize_filter_conditions(conditions)
2001 calls_df = pd.json_normalize(self.get_calls(limit=200, where_conditions=normalized_conditions))
2003 if calls_df.empty:
2004 raise ValueError("No calls retrieved from HubSpot to evaluate delete conditions.")
2006 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2007 delete_query_executor = DELETEQueryExecutor(calls_df, executor_conditions)
2008 filtered_df = delete_query_executor.execute_query()
2010 if filtered_df.empty:
2011 raise ValueError(f"No calls found matching WHERE conditions: {conditions}.")
2013 call_ids = filtered_df["id"].astype(str).tolist()
2014 logger.info(f"Deleting {len(call_ids)} call(s) matching WHERE conditions")
2015 self.delete_calls(call_ids)
2017 def get_columns(self) -> List[Text]:
2018 return self._get_default_call_columns()
2020 @staticmethod
2021 def _get_default_call_columns() -> List[str]:
2022 return [
2023 "id",
2024 "hs_call_title",
2025 "hs_call_body",
2026 "hs_call_direction",
2027 "hs_call_disposition",
2028 "hs_call_duration",
2029 "hs_call_status",
2030 "hubspot_owner_id",
2031 "hs_timestamp",
2032 "createdate",
2033 "lastmodifieddate",
2034 ]
2036 def get_calls(
2037 self,
2038 limit: Optional[int] = None,
2039 where_conditions: Optional[List] = None,
2040 properties: Optional[List[str]] = None,
2041 search_filters: Optional[List[Dict[str, Any]]] = None,
2042 search_sorts: Optional[List[Dict[str, Any]]] = None,
2043 allow_search: bool = True,
2044 **kwargs,
2045 ) -> List[Dict]:
2046 normalized_conditions = _normalize_filter_conditions(where_conditions)
2047 hubspot = self.handler.connect()
2049 requested_properties = properties or []
2050 default_properties = self._get_default_call_columns()
2051 columns = requested_properties or default_properties
2052 hubspot_properties = _build_hubspot_properties(columns)
2054 api_kwargs = {**kwargs, "properties": hubspot_properties}
2055 if limit is not None:
2056 api_kwargs["limit"] = limit
2057 else:
2058 api_kwargs.pop("limit", None)
2060 if allow_search and (search_filters or search_sorts or normalized_conditions):
2061 filters = search_filters
2062 if filters is None and normalized_conditions:
2063 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
2064 if filters is not None or search_sorts is not None:
2065 search_results = self._search_calls_by_conditions(
2066 hubspot, filters, hubspot_properties, limit, search_sorts, columns
2067 )
2068 logger.info(f"Retrieved {len(search_results)} calls from HubSpot via search API")
2069 return search_results
2071 calls = self.handler._get_objects_all("calls", **api_kwargs)
2072 calls_dict = []
2073 for call in calls:
2074 try:
2075 calls_dict.append(self._call_to_dict(call, columns))
2076 except Exception as e:
2077 logger.warning(f"Error processing call {getattr(call, 'id', 'unknown')}: {str(e)}")
2078 continue
2080 logger.info(f"Retrieved {len(calls_dict)} calls from HubSpot")
2081 return calls_dict
2083 def _search_calls_by_conditions(
2084 self,
2085 hubspot: HubSpot,
2086 filters: Optional[List[Dict[str, Any]]],
2087 properties: List[str],
2088 limit: Optional[int],
2089 sorts: Optional[List[Dict[str, Any]]],
2090 columns: List[str],
2091 ) -> List[Dict[str, Any]]:
2092 return _execute_hubspot_search(
2093 hubspot.crm.objects.search_api,
2094 filters or [],
2095 properties,
2096 limit,
2097 lambda obj: self._call_to_dict(obj, columns),
2098 sorts=sorts,
2099 object_type="calls",
2100 )
2102 def _call_to_dict(self, call: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
2103 columns = columns or self._get_default_call_columns()
2104 return self._object_to_dict(call, columns)
2106 def create_calls(self, calls_data: List[Dict[Text, Any]]) -> None:
2107 if not calls_data:
2108 raise ValueError("No call data provided for creation")
2110 logger.info(f"Attempting to create {len(calls_data)} call(s)")
2111 hubspot = self.handler.connect()
2112 calls_to_create = [HubSpotObjectInputCreate(properties=call) for call in calls_data]
2113 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=calls_to_create)
2115 try:
2116 created_calls = hubspot.crm.objects.calls.batch_api.create(
2117 batch_input_simple_public_object_batch_input_for_create=batch_input
2118 )
2119 if not created_calls or not hasattr(created_calls, "results") or not created_calls.results:
2120 raise Exception("Call creation returned no results")
2121 created_ids = [c.id for c in created_calls.results]
2122 logger.info(f"Successfully created {len(created_ids)} call(s) with IDs: {created_ids}")
2123 except Exception as e:
2124 logger.error(f"Calls creation failed: {str(e)}")
2125 raise Exception(f"Calls creation failed {e}")
2127 def update_calls(self, call_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
2128 hubspot = self.handler.connect()
2129 calls_to_update = [HubSpotObjectBatchInput(id=cid, properties=values_to_update) for cid in call_ids]
2130 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=calls_to_update)
2131 try:
2132 updated = hubspot.crm.objects.calls.batch_api.update(
2133 batch_input_simple_public_object_batch_input=batch_input
2134 )
2135 logger.info(f"Calls with ID {[c.id for c in updated.results]} updated")
2136 except Exception as e:
2137 raise Exception(f"Calls update failed {e}")
2139 def delete_calls(self, call_ids: List[Text]) -> None:
2140 hubspot = self.handler.connect()
2141 calls_to_delete = [HubSpotObjectId(id=cid) for cid in call_ids]
2142 batch_input = BatchInputSimplePublicObjectId(inputs=calls_to_delete)
2143 try:
2144 hubspot.crm.objects.calls.batch_api.archive(batch_input_simple_public_object_id=batch_input)
2145 logger.info("Calls deleted")
2146 except Exception as e:
2147 raise Exception(f"Calls deletion failed {e}")
2150class EmailsTable(HubSpotAPIResource):
2151 """HubSpot Emails table for email engagement logs."""
2153 SEARCHABLE_COLUMNS = {"hs_email_subject", "hs_email_direction", "hs_email_status", "id"}
2155 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
2156 row_count = None
2157 try:
2158 self.handler.connect()
2159 row_count = self.handler._estimate_table_rows("emails")
2160 except Exception as e:
2161 logger.warning(f"Could not estimate HubSpot emails row count: {e}")
2163 return {
2164 "TABLE_NAME": "emails",
2165 "TABLE_TYPE": "BASE TABLE",
2166 "TABLE_DESCRIPTION": "HubSpot email logs including subject, direction, status and content",
2167 "ROW_COUNT": row_count,
2168 }
2170 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
2171 return self.handler._get_default_meta_columns("emails")
2173 def list(
2174 self,
2175 conditions: List[FilterCondition] = None,
2176 limit: int = None,
2177 sort: List[SortColumn] = None,
2178 targets: List[str] = None,
2179 search_filters: Optional[List[Dict[str, Any]]] = None,
2180 search_sorts: Optional[List[Dict[str, Any]]] = None,
2181 allow_search: bool = True,
2182 ) -> pd.DataFrame:
2183 emails_df = pd.json_normalize(
2184 self.get_emails(
2185 limit=limit,
2186 where_conditions=conditions,
2187 properties=targets,
2188 search_filters=search_filters,
2189 search_sorts=search_sorts,
2190 allow_search=allow_search,
2191 )
2192 )
2193 if emails_df.empty:
2194 emails_df = pd.DataFrame(columns=targets or self._get_default_email_columns())
2195 return emails_df
2197 def add(self, email_data: List[dict]):
2198 self.create_emails(email_data)
2200 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
2201 normalized_conditions = _normalize_filter_conditions(conditions)
2202 emails_df = pd.json_normalize(self.get_emails(limit=200, where_conditions=normalized_conditions))
2204 if emails_df.empty:
2205 raise ValueError("No emails retrieved from HubSpot to evaluate update conditions.")
2207 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2208 update_query_executor = UPDATEQueryExecutor(emails_df, executor_conditions)
2209 filtered_df = update_query_executor.execute_query()
2211 if filtered_df.empty:
2212 raise ValueError(f"No emails found matching WHERE conditions: {conditions}.")
2214 email_ids = filtered_df["id"].astype(str).tolist()
2215 logger.info(f"Updating {len(email_ids)} email(s) matching WHERE conditions")
2216 self.update_emails(email_ids, values)
2218 def remove(self, conditions: List[FilterCondition]) -> None:
2219 normalized_conditions = _normalize_filter_conditions(conditions)
2220 emails_df = pd.json_normalize(self.get_emails(limit=200, where_conditions=normalized_conditions))
2222 if emails_df.empty:
2223 raise ValueError("No emails retrieved from HubSpot to evaluate delete conditions.")
2225 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2226 delete_query_executor = DELETEQueryExecutor(emails_df, executor_conditions)
2227 filtered_df = delete_query_executor.execute_query()
2229 if filtered_df.empty:
2230 raise ValueError(f"No emails found matching WHERE conditions: {conditions}.")
2232 email_ids = filtered_df["id"].astype(str).tolist()
2233 logger.info(f"Deleting {len(email_ids)} email(s) matching WHERE conditions")
2234 self.delete_emails(email_ids)
2236 def get_columns(self) -> List[Text]:
2237 return self._get_default_email_columns()
2239 @staticmethod
2240 def _get_default_email_columns() -> List[str]:
2241 return [
2242 "id",
2243 "hs_email_subject",
2244 "hs_email_text",
2245 "hs_email_direction",
2246 "hs_email_status",
2247 "hs_email_sender_email",
2248 "hs_email_to_email",
2249 "hubspot_owner_id",
2250 "hs_timestamp",
2251 "createdate",
2252 "lastmodifieddate",
2253 ]
2255 def get_emails(
2256 self,
2257 limit: Optional[int] = None,
2258 where_conditions: Optional[List] = None,
2259 properties: Optional[List[str]] = None,
2260 search_filters: Optional[List[Dict[str, Any]]] = None,
2261 search_sorts: Optional[List[Dict[str, Any]]] = None,
2262 allow_search: bool = True,
2263 **kwargs,
2264 ) -> List[Dict]:
2265 normalized_conditions = _normalize_filter_conditions(where_conditions)
2266 hubspot = self.handler.connect()
2268 requested_properties = properties or []
2269 default_properties = self._get_default_email_columns()
2270 columns = requested_properties or default_properties
2271 hubspot_properties = _build_hubspot_properties(columns)
2273 api_kwargs = {**kwargs, "properties": hubspot_properties}
2274 if limit is not None:
2275 api_kwargs["limit"] = limit
2276 else:
2277 api_kwargs.pop("limit", None)
2279 if allow_search and (search_filters or search_sorts or normalized_conditions):
2280 filters = search_filters
2281 if filters is None and normalized_conditions:
2282 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
2283 if filters is not None or search_sorts is not None:
2284 search_results = self._search_emails_by_conditions(
2285 hubspot, filters, hubspot_properties, limit, search_sorts, columns
2286 )
2287 logger.info(f"Retrieved {len(search_results)} emails from HubSpot via search API")
2288 return search_results
2290 emails = self.handler._get_objects_all("emails", **api_kwargs)
2291 emails_dict = []
2292 for email in emails:
2293 try:
2294 emails_dict.append(self._email_to_dict(email, columns))
2295 except Exception as e:
2296 logger.warning(f"Error processing email {getattr(email, 'id', 'unknown')}: {str(e)}")
2297 continue
2299 logger.info(f"Retrieved {len(emails_dict)} emails from HubSpot")
2300 return emails_dict
2302 def _search_emails_by_conditions(
2303 self,
2304 hubspot: HubSpot,
2305 filters: Optional[List[Dict[str, Any]]],
2306 properties: List[str],
2307 limit: Optional[int],
2308 sorts: Optional[List[Dict[str, Any]]],
2309 columns: List[str],
2310 ) -> List[Dict[str, Any]]:
2311 return _execute_hubspot_search(
2312 hubspot.crm.objects.search_api,
2313 filters or [],
2314 properties,
2315 limit,
2316 lambda obj: self._email_to_dict(obj, columns),
2317 sorts=sorts,
2318 object_type="emails",
2319 )
2321 def _email_to_dict(self, email: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
2322 columns = columns or self._get_default_email_columns()
2323 return self._object_to_dict(email, columns)
2325 def create_emails(self, emails_data: List[Dict[Text, Any]]) -> None:
2326 if not emails_data:
2327 raise ValueError("No email data provided for creation")
2329 logger.info(f"Attempting to create {len(emails_data)} email(s)")
2330 hubspot = self.handler.connect()
2331 emails_to_create = [HubSpotObjectInputCreate(properties=email) for email in emails_data]
2332 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=emails_to_create)
2334 try:
2335 created_emails = hubspot.crm.objects.emails.batch_api.create(
2336 batch_input_simple_public_object_batch_input_for_create=batch_input
2337 )
2338 if not created_emails or not hasattr(created_emails, "results") or not created_emails.results:
2339 raise Exception("Email creation returned no results")
2340 created_ids = [e.id for e in created_emails.results]
2341 logger.info(f"Successfully created {len(created_ids)} email(s) with IDs: {created_ids}")
2342 except Exception as e:
2343 logger.error(f"Emails creation failed: {str(e)}")
2344 raise Exception(f"Emails creation failed {e}")
2346 def update_emails(self, email_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
2347 hubspot = self.handler.connect()
2348 emails_to_update = [HubSpotObjectBatchInput(id=eid, properties=values_to_update) for eid in email_ids]
2349 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=emails_to_update)
2350 try:
2351 updated = hubspot.crm.objects.emails.batch_api.update(
2352 batch_input_simple_public_object_batch_input=batch_input
2353 )
2354 logger.info(f"Emails with ID {[e.id for e in updated.results]} updated")
2355 except Exception as e:
2356 raise Exception(f"Emails update failed {e}")
2358 def delete_emails(self, email_ids: List[Text]) -> None:
2359 hubspot = self.handler.connect()
2360 emails_to_delete = [HubSpotObjectId(id=eid) for eid in email_ids]
2361 batch_input = BatchInputSimplePublicObjectId(inputs=emails_to_delete)
2362 try:
2363 hubspot.crm.objects.emails.batch_api.archive(batch_input_simple_public_object_id=batch_input)
2364 logger.info("Emails deleted")
2365 except Exception as e:
2366 raise Exception(f"Emails deletion failed {e}")
2369class MeetingsTable(HubSpotAPIResource):
2370 """HubSpot Meetings table for meeting logs and scheduled meetings."""
2372 SEARCHABLE_COLUMNS = {"hs_meeting_title", "hs_meeting_outcome", "id"}
2374 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
2375 row_count = None
2376 try:
2377 self.handler.connect()
2378 row_count = self.handler._estimate_table_rows("meetings")
2379 except Exception as e:
2380 logger.warning(f"Could not estimate HubSpot meetings row count: {e}")
2382 return {
2383 "TABLE_NAME": "meetings",
2384 "TABLE_TYPE": "BASE TABLE",
2385 "TABLE_DESCRIPTION": "HubSpot meeting logs including title, location, outcome and timing",
2386 "ROW_COUNT": row_count,
2387 }
2389 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
2390 return self.handler._get_default_meta_columns("meetings")
2392 def list(
2393 self,
2394 conditions: List[FilterCondition] = None,
2395 limit: int = None,
2396 sort: List[SortColumn] = None,
2397 targets: List[str] = None,
2398 search_filters: Optional[List[Dict[str, Any]]] = None,
2399 search_sorts: Optional[List[Dict[str, Any]]] = None,
2400 allow_search: bool = True,
2401 ) -> pd.DataFrame:
2402 meetings_df = pd.json_normalize(
2403 self.get_meetings(
2404 limit=limit,
2405 where_conditions=conditions,
2406 properties=targets,
2407 search_filters=search_filters,
2408 search_sorts=search_sorts,
2409 allow_search=allow_search,
2410 )
2411 )
2412 if meetings_df.empty:
2413 meetings_df = pd.DataFrame(columns=targets or self._get_default_meeting_columns())
2414 return meetings_df
2416 def add(self, meeting_data: List[dict]):
2417 self.create_meetings(meeting_data)
2419 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
2420 normalized_conditions = _normalize_filter_conditions(conditions)
2421 meetings_df = pd.json_normalize(self.get_meetings(limit=200, where_conditions=normalized_conditions))
2423 if meetings_df.empty:
2424 raise ValueError("No meetings retrieved from HubSpot to evaluate update conditions.")
2426 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2427 update_query_executor = UPDATEQueryExecutor(meetings_df, executor_conditions)
2428 filtered_df = update_query_executor.execute_query()
2430 if filtered_df.empty:
2431 raise ValueError(f"No meetings found matching WHERE conditions: {conditions}.")
2433 meeting_ids = filtered_df["id"].astype(str).tolist()
2434 logger.info(f"Updating {len(meeting_ids)} meeting(s) matching WHERE conditions")
2435 self.update_meetings(meeting_ids, values)
2437 def remove(self, conditions: List[FilterCondition]) -> None:
2438 normalized_conditions = _normalize_filter_conditions(conditions)
2439 meetings_df = pd.json_normalize(self.get_meetings(limit=200, where_conditions=normalized_conditions))
2441 if meetings_df.empty:
2442 raise ValueError("No meetings retrieved from HubSpot to evaluate delete conditions.")
2444 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2445 delete_query_executor = DELETEQueryExecutor(meetings_df, executor_conditions)
2446 filtered_df = delete_query_executor.execute_query()
2448 if filtered_df.empty:
2449 raise ValueError(f"No meetings found matching WHERE conditions: {conditions}.")
2451 meeting_ids = filtered_df["id"].astype(str).tolist()
2452 logger.info(f"Deleting {len(meeting_ids)} meeting(s) matching WHERE conditions")
2453 self.delete_meetings(meeting_ids)
2455 def get_columns(self) -> List[Text]:
2456 return self._get_default_meeting_columns()
2458 @staticmethod
2459 def _get_default_meeting_columns() -> List[str]:
2460 return [
2461 "id",
2462 "hs_meeting_title",
2463 "hs_meeting_body",
2464 "hs_meeting_location",
2465 "hs_meeting_outcome",
2466 "hs_meeting_start_time",
2467 "hs_meeting_end_time",
2468 "hubspot_owner_id",
2469 "hs_timestamp",
2470 "createdate",
2471 "lastmodifieddate",
2472 ]
2474 def get_meetings(
2475 self,
2476 limit: Optional[int] = None,
2477 where_conditions: Optional[List] = None,
2478 properties: Optional[List[str]] = None,
2479 search_filters: Optional[List[Dict[str, Any]]] = None,
2480 search_sorts: Optional[List[Dict[str, Any]]] = None,
2481 allow_search: bool = True,
2482 **kwargs,
2483 ) -> List[Dict]:
2484 normalized_conditions = _normalize_filter_conditions(where_conditions)
2485 hubspot = self.handler.connect()
2487 requested_properties = properties or []
2488 default_properties = self._get_default_meeting_columns()
2489 columns = requested_properties or default_properties
2490 hubspot_properties = _build_hubspot_properties(columns)
2492 api_kwargs = {**kwargs, "properties": hubspot_properties}
2493 if limit is not None:
2494 api_kwargs["limit"] = limit
2495 else:
2496 api_kwargs.pop("limit", None)
2498 if allow_search and (search_filters or search_sorts or normalized_conditions):
2499 filters = search_filters
2500 if filters is None and normalized_conditions:
2501 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
2502 if filters is not None or search_sorts is not None:
2503 search_results = self._search_meetings_by_conditions(
2504 hubspot, filters, hubspot_properties, limit, search_sorts, columns
2505 )
2506 logger.info(f"Retrieved {len(search_results)} meetings from HubSpot via search API")
2507 return search_results
2509 meetings = self.handler._get_objects_all("meetings", **api_kwargs)
2510 meetings_dict = []
2511 for meeting in meetings:
2512 try:
2513 meetings_dict.append(self._meeting_to_dict(meeting, columns))
2514 except Exception as e:
2515 logger.warning(f"Error processing meeting {getattr(meeting, 'id', 'unknown')}: {str(e)}")
2516 continue
2518 logger.info(f"Retrieved {len(meetings_dict)} meetings from HubSpot")
2519 return meetings_dict
2521 def _search_meetings_by_conditions(
2522 self,
2523 hubspot: HubSpot,
2524 filters: Optional[List[Dict[str, Any]]],
2525 properties: List[str],
2526 limit: Optional[int],
2527 sorts: Optional[List[Dict[str, Any]]],
2528 columns: List[str],
2529 ) -> List[Dict[str, Any]]:
2530 return _execute_hubspot_search(
2531 hubspot.crm.objects.search_api,
2532 filters or [],
2533 properties,
2534 limit,
2535 lambda obj: self._meeting_to_dict(obj, columns),
2536 sorts=sorts,
2537 object_type="meetings",
2538 )
2540 def _meeting_to_dict(self, meeting: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
2541 columns = columns or self._get_default_meeting_columns()
2542 return self._object_to_dict(meeting, columns)
2544 def create_meetings(self, meetings_data: List[Dict[Text, Any]]) -> None:
2545 if not meetings_data:
2546 raise ValueError("No meeting data provided for creation")
2548 logger.info(f"Attempting to create {len(meetings_data)} meeting(s)")
2549 hubspot = self.handler.connect()
2550 meetings_to_create = [HubSpotObjectInputCreate(properties=meeting) for meeting in meetings_data]
2551 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=meetings_to_create)
2553 try:
2554 created_meetings = hubspot.crm.objects.meetings.batch_api.create(
2555 batch_input_simple_public_object_batch_input_for_create=batch_input
2556 )
2557 if not created_meetings or not hasattr(created_meetings, "results") or not created_meetings.results:
2558 raise Exception("Meeting creation returned no results")
2559 created_ids = [m.id for m in created_meetings.results]
2560 logger.info(f"Successfully created {len(created_ids)} meeting(s) with IDs: {created_ids}")
2561 except Exception as e:
2562 logger.error(f"Meetings creation failed: {str(e)}")
2563 raise Exception(f"Meetings creation failed {e}")
2565 def update_meetings(self, meeting_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
2566 hubspot = self.handler.connect()
2567 meetings_to_update = [HubSpotObjectBatchInput(id=mid, properties=values_to_update) for mid in meeting_ids]
2568 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=meetings_to_update)
2569 try:
2570 updated = hubspot.crm.objects.meetings.batch_api.update(
2571 batch_input_simple_public_object_batch_input=batch_input
2572 )
2573 logger.info(f"Meetings with ID {[m.id for m in updated.results]} updated")
2574 except Exception as e:
2575 raise Exception(f"Meetings update failed {e}")
2577 def delete_meetings(self, meeting_ids: List[Text]) -> None:
2578 hubspot = self.handler.connect()
2579 meetings_to_delete = [HubSpotObjectId(id=mid) for mid in meeting_ids]
2580 batch_input = BatchInputSimplePublicObjectId(inputs=meetings_to_delete)
2581 try:
2582 hubspot.crm.objects.meetings.batch_api.archive(batch_input_simple_public_object_id=batch_input)
2583 logger.info("Meetings deleted")
2584 except Exception as e:
2585 raise Exception(f"Meetings deletion failed {e}")
2588class NotesTable(HubSpotAPIResource):
2589 """HubSpot Notes table for timeline notes on records."""
2591 SEARCHABLE_COLUMNS = {"id"}
2593 def meta_get_tables(self, table_name: str) -> Dict[str, Any]:
2594 row_count = None
2595 try:
2596 self.handler.connect()
2597 row_count = self.handler._estimate_table_rows("notes")
2598 except Exception as e:
2599 logger.warning(f"Could not estimate HubSpot notes row count: {e}")
2601 return {
2602 "TABLE_NAME": "notes",
2603 "TABLE_TYPE": "BASE TABLE",
2604 "TABLE_DESCRIPTION": "HubSpot notes for timeline entries on records",
2605 "ROW_COUNT": row_count,
2606 }
2608 def meta_get_columns(self, table_name: str) -> List[Dict[str, Any]]:
2609 return self.handler._get_default_meta_columns("notes")
2611 def list(
2612 self,
2613 conditions: List[FilterCondition] = None,
2614 limit: int = None,
2615 sort: List[SortColumn] = None,
2616 targets: List[str] = None,
2617 search_filters: Optional[List[Dict[str, Any]]] = None,
2618 search_sorts: Optional[List[Dict[str, Any]]] = None,
2619 allow_search: bool = True,
2620 ) -> pd.DataFrame:
2621 notes_df = pd.json_normalize(
2622 self.get_notes(
2623 limit=limit,
2624 where_conditions=conditions,
2625 properties=targets,
2626 search_filters=search_filters,
2627 search_sorts=search_sorts,
2628 allow_search=allow_search,
2629 )
2630 )
2631 if notes_df.empty:
2632 notes_df = pd.DataFrame(columns=targets or self._get_default_note_columns())
2633 return notes_df
2635 def add(self, note_data: List[dict]):
2636 self.create_notes(note_data)
2638 def modify(self, conditions: List[FilterCondition], values: Dict) -> None:
2639 normalized_conditions = _normalize_filter_conditions(conditions)
2640 notes_df = pd.json_normalize(self.get_notes(limit=200, where_conditions=normalized_conditions))
2642 if notes_df.empty:
2643 raise ValueError("No notes retrieved from HubSpot to evaluate update conditions.")
2645 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2646 update_query_executor = UPDATEQueryExecutor(notes_df, executor_conditions)
2647 filtered_df = update_query_executor.execute_query()
2649 if filtered_df.empty:
2650 raise ValueError(f"No notes found matching WHERE conditions: {conditions}.")
2652 note_ids = filtered_df["id"].astype(str).tolist()
2653 logger.info(f"Updating {len(note_ids)} note(s) matching WHERE conditions")
2654 self.update_notes(note_ids, values)
2656 def remove(self, conditions: List[FilterCondition]) -> None:
2657 normalized_conditions = _normalize_filter_conditions(conditions)
2658 notes_df = pd.json_normalize(self.get_notes(limit=200, where_conditions=normalized_conditions))
2660 if notes_df.empty:
2661 raise ValueError("No notes retrieved from HubSpot to evaluate delete conditions.")
2663 executor_conditions = _normalize_conditions_for_executor(normalized_conditions)
2664 delete_query_executor = DELETEQueryExecutor(notes_df, executor_conditions)
2665 filtered_df = delete_query_executor.execute_query()
2667 if filtered_df.empty:
2668 raise ValueError(f"No notes found matching WHERE conditions: {conditions}.")
2670 note_ids = filtered_df["id"].astype(str).tolist()
2671 logger.info(f"Deleting {len(note_ids)} note(s) matching WHERE conditions")
2672 self.delete_notes(note_ids)
2674 def get_columns(self) -> List[Text]:
2675 return self._get_default_note_columns()
2677 @staticmethod
2678 def _get_default_note_columns() -> List[str]:
2679 return ["id", "hs_note_body", "hubspot_owner_id", "hs_timestamp", "createdate", "lastmodifieddate"]
2681 def get_notes(
2682 self,
2683 limit: Optional[int] = None,
2684 where_conditions: Optional[List] = None,
2685 properties: Optional[List[str]] = None,
2686 search_filters: Optional[List[Dict[str, Any]]] = None,
2687 search_sorts: Optional[List[Dict[str, Any]]] = None,
2688 allow_search: bool = True,
2689 **kwargs,
2690 ) -> List[Dict]:
2691 normalized_conditions = _normalize_filter_conditions(where_conditions)
2692 hubspot = self.handler.connect()
2694 requested_properties = properties or []
2695 default_properties = self._get_default_note_columns()
2696 columns = requested_properties or default_properties
2697 hubspot_properties = _build_hubspot_properties(columns)
2699 api_kwargs = {**kwargs, "properties": hubspot_properties}
2700 if limit is not None:
2701 api_kwargs["limit"] = limit
2702 else:
2703 api_kwargs.pop("limit", None)
2705 if allow_search and (search_filters or search_sorts or normalized_conditions):
2706 filters = search_filters
2707 if filters is None and normalized_conditions:
2708 filters = _build_hubspot_search_filters(normalized_conditions, self.SEARCHABLE_COLUMNS)
2709 if filters is not None or search_sorts is not None:
2710 search_results = self._search_notes_by_conditions(
2711 hubspot, filters, hubspot_properties, limit, search_sorts, columns
2712 )
2713 logger.info(f"Retrieved {len(search_results)} notes from HubSpot via search API")
2714 return search_results
2716 notes = self.handler._get_objects_all("notes", **api_kwargs)
2717 notes_dict = []
2718 for note in notes:
2719 try:
2720 notes_dict.append(self._note_to_dict(note, columns))
2721 except Exception as e:
2722 logger.warning(f"Error processing note {getattr(note, 'id', 'unknown')}: {str(e)}")
2723 continue
2725 logger.info(f"Retrieved {len(notes_dict)} notes from HubSpot")
2726 return notes_dict
2728 def _search_notes_by_conditions(
2729 self,
2730 hubspot: HubSpot,
2731 filters: Optional[List[Dict[str, Any]]],
2732 properties: List[str],
2733 limit: Optional[int],
2734 sorts: Optional[List[Dict[str, Any]]],
2735 columns: List[str],
2736 ) -> List[Dict[str, Any]]:
2737 return _execute_hubspot_search(
2738 hubspot.crm.objects.search_api,
2739 filters or [],
2740 properties,
2741 limit,
2742 lambda obj: self._note_to_dict(obj, columns),
2743 sorts=sorts,
2744 object_type="notes",
2745 )
2747 def _note_to_dict(self, note: Any, columns: Optional[List[str]] = None) -> Dict[str, Any]:
2748 columns = columns or self._get_default_note_columns()
2749 return self._object_to_dict(note, columns)
2751 def create_notes(self, notes_data: List[Dict[Text, Any]]) -> None:
2752 if not notes_data:
2753 raise ValueError("No note data provided for creation")
2755 logger.info(f"Attempting to create {len(notes_data)} note(s)")
2756 hubspot = self.handler.connect()
2757 notes_to_create = [HubSpotObjectInputCreate(properties=note) for note in notes_data]
2758 batch_input = BatchInputSimplePublicObjectBatchInputForCreate(inputs=notes_to_create)
2760 try:
2761 created_notes = hubspot.crm.objects.notes.batch_api.create(
2762 batch_input_simple_public_object_batch_input_for_create=batch_input
2763 )
2764 if not created_notes or not hasattr(created_notes, "results") or not created_notes.results:
2765 raise Exception("Note creation returned no results")
2766 created_ids = [n.id for n in created_notes.results]
2767 logger.info(f"Successfully created {len(created_ids)} note(s) with IDs: {created_ids}")
2768 except Exception as e:
2769 logger.error(f"Notes creation failed: {str(e)}")
2770 raise Exception(f"Notes creation failed {e}")
2772 def update_notes(self, note_ids: List[Text], values_to_update: Dict[Text, Any]) -> None:
2773 hubspot = self.handler.connect()
2774 notes_to_update = [HubSpotObjectBatchInput(id=nid, properties=values_to_update) for nid in note_ids]
2775 batch_input = BatchInputSimplePublicObjectBatchInput(inputs=notes_to_update)
2776 try:
2777 updated = hubspot.crm.objects.notes.batch_api.update(
2778 batch_input_simple_public_object_batch_input=batch_input
2779 )
2780 logger.info(f"Notes with ID {[n.id for n in updated.results]} updated")
2781 except Exception as e:
2782 raise Exception(f"Notes update failed {e}")
2784 def delete_notes(self, note_ids: List[Text]) -> None:
2785 hubspot = self.handler.connect()
2786 notes_to_delete = [HubSpotObjectId(id=nid) for nid in note_ids]
2787 batch_input = BatchInputSimplePublicObjectId(inputs=notes_to_delete)
2788 try:
2789 hubspot.crm.objects.notes.batch_api.archive(batch_input_simple_public_object_id=batch_input)
2790 logger.info("Notes deleted")
2791 except Exception as e:
2792 raise Exception(f"Notes deletion failed {e}")