diff --git a/answer_rocket/graphql/operations/layouts.gql b/answer_rocket/graphql/operations/layouts.gql index 61d1306..ba15699 100644 --- a/answer_rocket/graphql/operations/layouts.gql +++ b/answer_rocket/graphql/operations/layouts.gql @@ -7,4 +7,13 @@ query getDynamicLayout($id: UUID!) { name } } +} + +mutation GeneratePdfFromLayouts($layouts: [String!]!) { + generatePdfFromLayouts(layouts: $layouts) { + success + code + error + pdf + } } \ No newline at end of file diff --git a/answer_rocket/graphql/schema.py b/answer_rocket/graphql/schema.py index 64e345d..6b60dd3 100644 --- a/answer_rocket/graphql/schema.py +++ b/answer_rocket/graphql/schema.py @@ -762,6 +762,15 @@ class HydratedReport(sgqlc.types.Type): meta = sgqlc.types.Field(JSON, graphql_name='meta') +class LayoutPdfResponse(sgqlc.types.Type): + __schema__ = schema + __field_names__ = ('success', 'code', 'error', 'pdf') + success = sgqlc.types.Field(sgqlc.types.non_null(Boolean), graphql_name='success') + code = sgqlc.types.Field(String, graphql_name='code') + error = sgqlc.types.Field(String, graphql_name='error') + pdf = sgqlc.types.Field(String, graphql_name='pdf') + + class MatchValues(sgqlc.types.Type): __schema__ = schema __field_names__ = ('automatic_db_whitelist', 'constrained_values', 'dataset_id', 'default_performance_metric', 'inverse_map', 'phrase_template', 'popular_values', 'value_collection_name', 'dataset_date_dimensions', 'dataset_dimensions', 'dataset_metrics', 'predicate_vocab', 'defer_predicate_vocab_loading') @@ -1235,7 +1244,7 @@ class ModelOverrideResult(sgqlc.types.Type): class Mutation(sgqlc.types.Type): __schema__ = schema - __field_names__ = ('create_max_copilot_skill_chat_question', 'update_max_copilot_skill_chat_question', 'delete_max_copilot_skill_chat_question', 'create_max_copilot_question', 'update_max_copilot_question', 'delete_max_copilot_question', 'run_copilot_skill_async', 'clear_copilot_cache', 'create_copilot_test_run', 'run_copilot_test_run', 'cancel_copilot_test_run', 'set_max_agent_workflow', 'import_copilot_skill_from_zip', 'sync_max_skill_repository', 'import_skill_from_repo', 'test_run_copilot_skill', 'get_test_run_output', 'reload_dataset', 'update_database_name', 'update_database_description', 'update_database_llm_description', 'update_database_mermaid_er_diagram', 'update_database_kshot_limit', 'update_dataset_name', 'update_dataset_description', 'update_dataset_date_range', 'update_dataset_data_interval', 'update_dataset_misc_info', 'update_dataset_source', 'update_dataset_query_row_limit', 'update_dataset_use_database_casing', 'update_dataset_kshot_limit', 'create_dataset', 'create_dataset_from_table', 'create_dimension', 'update_dimension', 'delete_dimension', 'create_metric', 'update_metric', 'delete_metric', 'create_database_kshot', 'update_database_kshot_question', 'update_database_kshot_rendered_prompt', 'update_database_kshot_explanation', 'update_database_kshot_sql', 'update_database_kshot_title', 'update_database_kshot_visualization', 'delete_database_kshot', 'create_dataset_kshot', 'update_dataset_kshot_question', 'update_dataset_kshot_rendered_prompt', 'update_dataset_kshot_explanation', 'update_dataset_kshot_sql', 'update_dataset_kshot_title', 'update_dataset_kshot_visualization', 'delete_dataset_kshot', 'update_chat_answer_payload', 'ask_chat_question', 'evaluate_chat_question', 'queue_chat_question', 'cancel_chat_question', 'create_chat_thread', 'add_feedback', 'set_skill_memory', 'share_thread', 'update_loading_message', 'create_chat_artifact', 'delete_chat_artifact', 'send_email') + __field_names__ = ('create_max_copilot_skill_chat_question', 'update_max_copilot_skill_chat_question', 'delete_max_copilot_skill_chat_question', 'create_max_copilot_question', 'update_max_copilot_question', 'delete_max_copilot_question', 'run_copilot_skill_async', 'clear_copilot_cache', 'create_copilot_test_run', 'run_copilot_test_run', 'cancel_copilot_test_run', 'set_max_agent_workflow', 'import_copilot_skill_from_zip', 'sync_max_skill_repository', 'import_skill_from_repo', 'test_run_copilot_skill', 'get_test_run_output', 'reload_dataset', 'update_database_name', 'update_database_description', 'update_database_llm_description', 'update_database_mermaid_er_diagram', 'update_database_kshot_limit', 'update_dataset_name', 'update_dataset_description', 'update_dataset_date_range', 'update_dataset_data_interval', 'update_dataset_misc_info', 'update_dataset_source', 'update_dataset_query_row_limit', 'update_dataset_use_database_casing', 'update_dataset_kshot_limit', 'create_dataset', 'create_dataset_from_table', 'create_dimension', 'update_dimension', 'delete_dimension', 'create_metric', 'update_metric', 'delete_metric', 'create_database_kshot', 'update_database_kshot_question', 'update_database_kshot_rendered_prompt', 'update_database_kshot_explanation', 'update_database_kshot_sql', 'update_database_kshot_title', 'update_database_kshot_visualization', 'delete_database_kshot', 'create_dataset_kshot', 'update_dataset_kshot_question', 'update_dataset_kshot_rendered_prompt', 'update_dataset_kshot_explanation', 'update_dataset_kshot_sql', 'update_dataset_kshot_title', 'update_dataset_kshot_visualization', 'delete_dataset_kshot', 'update_chat_answer_payload', 'ask_chat_question', 'evaluate_chat_question', 'queue_chat_question', 'cancel_chat_question', 'create_chat_thread', 'add_feedback', 'set_skill_memory', 'share_thread', 'update_loading_message', 'create_chat_artifact', 'delete_chat_artifact', 'generate_pdf_from_layouts', 'send_email') create_max_copilot_skill_chat_question = sgqlc.types.Field(sgqlc.types.non_null(CreateMaxCopilotSkillChatQuestionResponse), graphql_name='createMaxCopilotSkillChatQuestion', args=sgqlc.types.ArgDict(( ('copilot_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='copilotId', default=None)), ('copilot_skill_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='copilotSkillId', default=None)), @@ -1603,6 +1612,10 @@ class Mutation(sgqlc.types.Type): ) delete_chat_artifact = sgqlc.types.Field(sgqlc.types.non_null(MaxMutationResponse), graphql_name='deleteChatArtifact', args=sgqlc.types.ArgDict(( ('chat_artifact_id', sgqlc.types.Arg(sgqlc.types.non_null(UUID), graphql_name='chatArtifactId', default=None)), +)) + ) + generate_pdf_from_layouts = sgqlc.types.Field(sgqlc.types.non_null(LayoutPdfResponse), graphql_name='generatePdfFromLayouts', args=sgqlc.types.ArgDict(( + ('layouts', sgqlc.types.Arg(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(String))), graphql_name='layouts', default=None)), )) ) send_email = sgqlc.types.Field(sgqlc.types.non_null(EmailSendResponse), graphql_name='sendEmail', args=sgqlc.types.ArgDict(( diff --git a/answer_rocket/graphql/sdk_operations.py b/answer_rocket/graphql/sdk_operations.py index c973abd..89f4741 100644 --- a/answer_rocket/graphql/sdk_operations.py +++ b/answer_rocket/graphql/sdk_operations.py @@ -532,6 +532,16 @@ def mutation_send_email(): return _op +def mutation_generate_pdf_from_layouts(): + _op = sgqlc.operation.Operation(_schema_root.mutation_type, name='GeneratePdfFromLayouts', variables=dict(layouts=sgqlc.types.Arg(sgqlc.types.non_null(sgqlc.types.list_of(sgqlc.types.non_null(_schema.String)))))) + _op_generate_pdf_from_layouts = _op.generate_pdf_from_layouts(layouts=sgqlc.types.Variable('layouts')) + _op_generate_pdf_from_layouts.success() + _op_generate_pdf_from_layouts.code() + _op_generate_pdf_from_layouts.error() + _op_generate_pdf_from_layouts.pdf() + return _op + + def mutation_update_loading_message(): _op = sgqlc.operation.Operation(_schema_root.mutation_type, name='UpdateLoadingMessage', variables=dict(answerId=sgqlc.types.Arg(sgqlc.types.non_null(_schema.UUID)), message=sgqlc.types.Arg(sgqlc.types.non_null(_schema.String)))) _op.update_loading_message(answer_id=sgqlc.types.Variable('answerId'), message=sgqlc.types.Variable('message')) @@ -568,6 +578,7 @@ class Mutation: delete_dataset_kshot = mutation_delete_dataset_kshot() delete_dimension = mutation_delete_dimension() delete_metric = mutation_delete_metric() + generate_pdf_from_layouts = mutation_generate_pdf_from_layouts() get_test_run_output = mutation_get_test_run_output() import_copilot_skill_from_zip = mutation_import_copilot_skill_from_zip() import_skill_from_repo = mutation_import_skill_from_repo() diff --git a/answer_rocket/layouts.py b/answer_rocket/layouts.py index 3444879..21819d7 100644 --- a/answer_rocket/layouts.py +++ b/answer_rocket/layouts.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +from typing import Optional, Dict, Any, List +from uuid import UUID + from answer_rocket.client_config import ClientConfig from answer_rocket.graphql.client import GraphQlClient from answer_rocket.graphql.sdk_operations import Operations @@ -31,6 +36,90 @@ def get_dynamic_layout(self, id: str): }) return result.get_dynamic_layout - + except Exception as e: raise Exception(f"Failed to get dynamic layout: {e}") + + def generate_pdf_from_layouts( + self, + layouts: List[str] + ): + """ + Generate a PDF from a list of layout JSON strings. + + This is the primary method for SDK users to generate PDFs from layouts. + Uses the same rendering infrastructure as the "Download PDF" button in the UI, + ensuring identical output. Returns base64-encoded PDF data that can be + directly passed to sendEmail as an attachment. + + Parameters + ---------- + layouts : List[str] + List of layout JSON strings to render as PDF pages. + Each string should be a valid layout JSON representation. + + Returns + ------- + LayoutPdfResponse + Response object containing: + - success: bool - Whether the PDF generation completed successfully + - code: str - Optional status or error code + - error: str - Human-readable error message if the operation failed + - pdf: str - Base64-encoded PDF data, ready to use as an email attachment payload + + Examples + -------- + Generate a PDF from a single layout: + + >>> # First, get the layout + >>> layout = max.dynamic_layouts.get_dynamic_layout( + ... id="12345678-1234-1234-1234-123456789abc" + ... ) + >>> # Generate PDF from the layout JSON + >>> result = max.dynamic_layouts.generate_pdf_from_layouts( + ... layouts=[layout.layout_json] + ... ) + >>> if result.success: + ... import base64 + ... pdf_bytes = base64.b64decode(result.pdf) + ... with open('output.pdf', 'wb') as f: + ... f.write(pdf_bytes) + + Generate a multi-page PDF from multiple layouts: + + >>> layout1 = max.dynamic_layouts.get_dynamic_layout(id="layout-1-id") + >>> layout2 = max.dynamic_layouts.get_dynamic_layout(id="layout-2-id") + >>> result = max.dynamic_layouts.generate_pdf_from_layouts( + ... layouts=[layout1.layout_json, layout2.layout_json] + ... ) + >>> if result.success: + ... print(f"Generated multi-page PDF") + ... else: + ... print(f"Error: {result.error}") + + Use the PDF directly as an email attachment: + + >>> layout = max.dynamic_layouts.get_dynamic_layout(id="layout-id") + >>> result = max.dynamic_layouts.generate_pdf_from_layouts( + ... layouts=[layout.layout_json] + ... ) + >>> if result.success: + ... email_result = max.email.send_email( + ... user_ids=[uuid.UUID("user-id-here")], + ... subject="Your Report", + ... body="Please see the attached PDF report.", + ... attachments=[{ + ... 'filename': 'report.pdf', + ... 'payload': result.pdf, + ... 'type': 'application/pdf' + ... }] + ... ) + """ + mutation_args = { + 'layouts': layouts, + } + + op = Operations.mutation.generate_pdf_from_layouts + result = self._gql_client.submit(op, mutation_args) + + return result.generate_pdf_from_layouts