sf-queue-sdk 0.3.1b255__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/.gitignore +4 -0
  2. sf_queue_sdk-0.3.1b255/README.md → sf_queue_sdk-0.4.0/PKG-INFO +46 -18
  3. sf_queue_sdk-0.3.1b255/PKG-INFO → sf_queue_sdk-0.4.0/README.md +38 -26
  4. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/pyproject.toml +1 -1
  5. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/__init__.py +1 -1
  6. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/email/v1/email_pb2.py +6 -6
  7. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +8 -4
  8. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/document_notes_queue.py +20 -0
  9. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/document_upload_queue.py +5 -0
  10. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/email_queue.py +66 -12
  11. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/types.py +3 -0
  12. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/client.py +0 -0
  13. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/__init__.py +0 -0
  14. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.py +0 -0
  15. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.pyi +0 -0
  16. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.py +0 -0
  17. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.pyi +0 -0
  18. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.py +0 -0
  19. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.pyi +0 -0
  20. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/query.py +0 -0
  21. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/__init__.py +0 -0
  22. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/base_queue.py +0 -0
  23. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/document_persist_queue.py +0 -0
  24. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/pdf_to_image_queue.py +0 -0
  25. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/prompt_ingest_queue.py +0 -0
  26. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/recommendation_queue.py +0 -0
  27. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/tool_assignment_queue.py +0 -0
  28. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/queues/topic_assignment_queue.py +0 -0
  29. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/redis_client.py +0 -0
  30. {sf_queue_sdk-0.3.1b255 → sf_queue_sdk-0.4.0}/queue_sdk/sanitize.py +0 -0
@@ -55,3 +55,7 @@ coverage/
55
55
  # packages/ts/queue-sdk/src/generated/ -- committed
56
56
  # packages/py/queue-sdk/queue_sdk/generated/ -- committed
57
57
  TODO-document-upload-queue.md
58
+
59
+ # Local pipeline test output
60
+ scripts/.env
61
+ scripts/local-pdfs/
@@ -1,3 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: sf-queue-sdk
3
+ Version: 0.4.0
4
+ Summary: Python SDK for sf-queue - Redis-based queue system
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: redis>=5.0.0
7
+ Description-Content-Type: text/markdown
8
+
1
9
  # sf-queue-sdk
2
10
 
3
11
  Python SDK for sf-queue. Enqueues emails via Redis Streams with optional blocking confirmation from the Go consumer service.
@@ -144,7 +152,6 @@ print(result.error) # error message if all failed
144
152
  | `image` | `{"src": str, "alt"?: str, "width"?: int, "height"?: int}` | No | Image in email body |
145
153
  | `service` | `str` | No | Service name for routing (e.g. `"sparkles"`). Routes to service-specific credentials, from address, and template. Defaults to the base config when omitted. |
146
154
  | `template` | `str` | No | Template name override (e.g. `"sparkles"`). Overrides the service's default template. Falls back to `"studyfetch"` when omitted. |
147
- | `brand` | `{"name"?: str, "logoUrl"?: str, "url"?: str, "accentColor"?: str}` | No | Per-workspace whitelabel branding (Honen template). `name` sets the wordmark/footer text **and** the From display name (the sending address and Resend key are unchanged, preserving the verified domain). Also overrides the logo, footer + header + unsubscribe link base, and CTA color. Any omitted field falls back to the default Honen brand. |
148
155
 
149
156
  ## Optional Fields
150
157
 
@@ -192,31 +199,28 @@ client.email.send(
192
199
 
193
200
  ## Whitelabel Branding
194
201
 
195
- Pass `brand` to render the Honen template with a workspace's own identity. Any
196
- omitted field falls back to the default Honen brand, so unbranded emails are
197
- unaffected.
202
+ The Honen template accepts optional brand overrides. Empty fields retain the
203
+ standard Honen identity.
198
204
 
199
205
  ```python
200
206
  client.email.send(
201
207
  to="member@acme.com",
202
208
  preview="You have been invited",
203
- subject="Join the Acme workspace",
209
+ subject="Join Acme",
204
210
  paragraphs=["You have been invited to Acme."],
205
- button={"text": "Accept invite", "href": "https://learn.acme.com/invite/abc"},
211
+ service="honen",
212
+ template="honen",
206
213
  brand={
207
- "name": "Acme", # wordmark + From display name ("Acme <noreply@...>")
214
+ "name": "Acme",
208
215
  "logoUrl": "https://cdn.acme.com/logo.png",
209
- "url": "https://learn.acme.com", # header/footer/unsubscribe link base
210
- "accentColor": "#0043FF", # CTA button color
216
+ "url": "https://learn.acme.com",
217
+ "accentColor": "#0043FF",
211
218
  },
212
219
  )
213
220
  ```
214
221
 
215
- The `name` also becomes the From display name: the email arrives from
216
- `Acme <noreply@honen.studyfetch.com>` instead of `Honen <noreply@...>`. Only the
217
- display label changes — the verified sending address and the service's Resend
218
- key stay the same, so DKIM/SPF alignment is unaffected. Omit `name` (or the whole
219
- `brand`) to keep the default Honen From.
222
+ `name` also becomes the From display name while the selected route or validated
223
+ From override continues to control the sending address and provider key.
220
224
 
221
225
  ## Migrating from sendEmail / sendBatchEmail
222
226
 
@@ -594,7 +598,7 @@ result = client.document_upload.send(
594
598
  | `url` | `str` | Yes | Download URL for the source file |
595
599
  | `generate_notes` | `bool` | Yes | Whether to generate study notes |
596
600
  | `generate_study_path` | `bool` | Yes | Whether to generate a study path |
597
- | `splitting` | `str` | No | Splitting strategy (default: `"auto"`) |
601
+ | `splitting` | `str` | No | Splitting strategy for sources with at least 40,000 tokens (default: `"auto"`). Smaller sources always persist as one material after per-topic note generation. |
598
602
  | `language` | `str` | No | Target language code (default: `"en"`) |
599
603
  | `use_markdown_ocr` | `bool` | No | Use Markdown OCR for image interpretation (default: `False`) |
600
604
  | `flashcard_amount` | `int` | No | Number of flashcards to generate (default: `12`) |
@@ -608,6 +612,7 @@ result = client.document_upload.send(
608
612
  | `upload_batch_id` | `Optional[str]` | No | Batch ID when multiple files uploaded together |
609
613
  | `youtube_title` | `Optional[str]` | No | Title override for YouTube video transcripts |
610
614
  | `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
615
+ | `existing_material_id` | `Optional[str]` | No | ID of an existing material to link to topics instead of creating new ones (used with `skip_material_creation`) |
611
616
 
612
617
  ### Response Type
613
618
 
@@ -676,7 +681,7 @@ result = client.document_notes.send(
676
681
  uploaded_file_id="file_abc123",
677
682
  study_set_id="set_xyz",
678
683
  user_id="user_123",
679
- stash_key="clustering-stash/file_abc123/1720000000.json",
684
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
680
685
  is_deferred=True,
681
686
  callback_url="https://api.example.com/completeCluster",
682
687
  callback_api_key="secret",
@@ -692,7 +697,7 @@ result = client.document_notes.send(
692
697
  uploaded_file_id="file_abc123",
693
698
  study_set_id="set_xyz",
694
699
  user_id="user_123",
695
- stash_key="clustering-stash/file_abc123/1720000000.json",
700
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
696
701
  is_deferred=False,
697
702
  generate_notes=False,
698
703
  folder_id="folder_abc",
@@ -701,6 +706,25 @@ result = client.document_notes.send(
701
706
  )
702
707
  ```
703
708
 
709
+ ### Note regeneration for existing materials
710
+
711
+ Use `skip_material_creation` and `existing_material_id` to regenerate notes for an existing `noNotes` material using its original clustering stash. The stash key is deterministic: `clustering-stash/{study_set_id}/{uploaded_file_id}.json`.
712
+
713
+ ```python
714
+ result = client.document_notes.send(
715
+ uploaded_file_id="file_abc123",
716
+ study_set_id="set_xyz",
717
+ user_id="user_123",
718
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
719
+ is_deferred=False,
720
+ generate_notes=True,
721
+ skip_material_creation=True,
722
+ existing_material_id="mat_existing_456",
723
+ callback_url="https://api.example.com/noNotesGen/callback",
724
+ callback_api_key="secret",
725
+ )
726
+ ```
727
+
704
728
  ### Document Notes Fields
705
729
 
706
730
  | Field | Type | Required | Description |
@@ -708,12 +732,16 @@ result = client.document_notes.send(
708
732
  | `uploaded_file_id` | `str` | Yes | ID of the uploaded file record |
709
733
  | `study_set_id` | `str` | Yes | Study set to persist materials into |
710
734
  | `user_id` | `str` | Yes | User who uploaded the document |
711
- | `stash_key` | `str` | Yes | GCS key of the clustering stash JSON |
735
+ | `stash_key` | `str` | Yes | GCS key of the clustering stash JSON (deterministic: `clustering-stash/{study_set_id}/{uploaded_file_id}.json`) |
712
736
  | `is_deferred` | `bool` | No | Whether the study set has existing topics requiring approval (default: `False`) |
713
737
  | `generate_notes` | `bool` | No | Whether to generate notes (default: `True`). Set to `False` for noNotes materials |
714
738
  | `callback_url` | `Optional[str]` | No | URL to POST results to after completion |
715
739
  | `callback_api_key` | `Optional[str]` | No | Auth key for the callback |
716
740
  | `folder_id` | `Optional[str]` | No | Pre-created folder ID for the materials |
741
+ | `skip_material_creation` | `Optional[bool]` | No | Skip creating new materials; update an existing one instead (used for note regeneration) |
742
+ | `existing_material_id` | `Optional[str]` | No | ID of the existing material to update with generated notes (used with `skip_material_creation`) |
743
+ | `target_topic_index` | `Optional[int]` | No | Index of the specific topic to regenerate notes for (matches `material.order`). Omit to generate all topics. |
744
+ | `append_mode` | `Optional[bool]` | No | When `True`, generated notes are appended to existing material content rather than replacing it |
717
745
 
718
746
  ### Response Types
719
747
 
@@ -1,11 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: sf-queue-sdk
3
- Version: 0.3.1b255
4
- Summary: Python SDK for sf-queue - Redis-based queue system
5
- Requires-Python: >=3.11
6
- Requires-Dist: redis>=5.0.0
7
- Description-Content-Type: text/markdown
8
-
9
1
  # sf-queue-sdk
10
2
 
11
3
  Python SDK for sf-queue. Enqueues emails via Redis Streams with optional blocking confirmation from the Go consumer service.
@@ -152,7 +144,6 @@ print(result.error) # error message if all failed
152
144
  | `image` | `{"src": str, "alt"?: str, "width"?: int, "height"?: int}` | No | Image in email body |
153
145
  | `service` | `str` | No | Service name for routing (e.g. `"sparkles"`). Routes to service-specific credentials, from address, and template. Defaults to the base config when omitted. |
154
146
  | `template` | `str` | No | Template name override (e.g. `"sparkles"`). Overrides the service's default template. Falls back to `"studyfetch"` when omitted. |
155
- | `brand` | `{"name"?: str, "logoUrl"?: str, "url"?: str, "accentColor"?: str}` | No | Per-workspace whitelabel branding (Honen template). `name` sets the wordmark/footer text **and** the From display name (the sending address and Resend key are unchanged, preserving the verified domain). Also overrides the logo, footer + header + unsubscribe link base, and CTA color. Any omitted field falls back to the default Honen brand. |
156
147
 
157
148
  ## Optional Fields
158
149
 
@@ -200,31 +191,28 @@ client.email.send(
200
191
 
201
192
  ## Whitelabel Branding
202
193
 
203
- Pass `brand` to render the Honen template with a workspace's own identity. Any
204
- omitted field falls back to the default Honen brand, so unbranded emails are
205
- unaffected.
194
+ The Honen template accepts optional brand overrides. Empty fields retain the
195
+ standard Honen identity.
206
196
 
207
197
  ```python
208
198
  client.email.send(
209
199
  to="member@acme.com",
210
200
  preview="You have been invited",
211
- subject="Join the Acme workspace",
201
+ subject="Join Acme",
212
202
  paragraphs=["You have been invited to Acme."],
213
- button={"text": "Accept invite", "href": "https://learn.acme.com/invite/abc"},
203
+ service="honen",
204
+ template="honen",
214
205
  brand={
215
- "name": "Acme", # wordmark + From display name ("Acme <noreply@...>")
206
+ "name": "Acme",
216
207
  "logoUrl": "https://cdn.acme.com/logo.png",
217
- "url": "https://learn.acme.com", # header/footer/unsubscribe link base
218
- "accentColor": "#0043FF", # CTA button color
208
+ "url": "https://learn.acme.com",
209
+ "accentColor": "#0043FF",
219
210
  },
220
211
  )
221
212
  ```
222
213
 
223
- The `name` also becomes the From display name: the email arrives from
224
- `Acme <noreply@honen.studyfetch.com>` instead of `Honen <noreply@...>`. Only the
225
- display label changes — the verified sending address and the service's Resend
226
- key stay the same, so DKIM/SPF alignment is unaffected. Omit `name` (or the whole
227
- `brand`) to keep the default Honen From.
214
+ `name` also becomes the From display name while the selected route or validated
215
+ From override continues to control the sending address and provider key.
228
216
 
229
217
  ## Migrating from sendEmail / sendBatchEmail
230
218
 
@@ -602,7 +590,7 @@ result = client.document_upload.send(
602
590
  | `url` | `str` | Yes | Download URL for the source file |
603
591
  | `generate_notes` | `bool` | Yes | Whether to generate study notes |
604
592
  | `generate_study_path` | `bool` | Yes | Whether to generate a study path |
605
- | `splitting` | `str` | No | Splitting strategy (default: `"auto"`) |
593
+ | `splitting` | `str` | No | Splitting strategy for sources with at least 40,000 tokens (default: `"auto"`). Smaller sources always persist as one material after per-topic note generation. |
606
594
  | `language` | `str` | No | Target language code (default: `"en"`) |
607
595
  | `use_markdown_ocr` | `bool` | No | Use Markdown OCR for image interpretation (default: `False`) |
608
596
  | `flashcard_amount` | `int` | No | Number of flashcards to generate (default: `12`) |
@@ -616,6 +604,7 @@ result = client.document_upload.send(
616
604
  | `upload_batch_id` | `Optional[str]` | No | Batch ID when multiple files uploaded together |
617
605
  | `youtube_title` | `Optional[str]` | No | Title override for YouTube video transcripts |
618
606
  | `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
607
+ | `existing_material_id` | `Optional[str]` | No | ID of an existing material to link to topics instead of creating new ones (used with `skip_material_creation`) |
619
608
 
620
609
  ### Response Type
621
610
 
@@ -684,7 +673,7 @@ result = client.document_notes.send(
684
673
  uploaded_file_id="file_abc123",
685
674
  study_set_id="set_xyz",
686
675
  user_id="user_123",
687
- stash_key="clustering-stash/file_abc123/1720000000.json",
676
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
688
677
  is_deferred=True,
689
678
  callback_url="https://api.example.com/completeCluster",
690
679
  callback_api_key="secret",
@@ -700,7 +689,7 @@ result = client.document_notes.send(
700
689
  uploaded_file_id="file_abc123",
701
690
  study_set_id="set_xyz",
702
691
  user_id="user_123",
703
- stash_key="clustering-stash/file_abc123/1720000000.json",
692
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
704
693
  is_deferred=False,
705
694
  generate_notes=False,
706
695
  folder_id="folder_abc",
@@ -709,6 +698,25 @@ result = client.document_notes.send(
709
698
  )
710
699
  ```
711
700
 
701
+ ### Note regeneration for existing materials
702
+
703
+ Use `skip_material_creation` and `existing_material_id` to regenerate notes for an existing `noNotes` material using its original clustering stash. The stash key is deterministic: `clustering-stash/{study_set_id}/{uploaded_file_id}.json`.
704
+
705
+ ```python
706
+ result = client.document_notes.send(
707
+ uploaded_file_id="file_abc123",
708
+ study_set_id="set_xyz",
709
+ user_id="user_123",
710
+ stash_key="clustering-stash/set_xyz/file_abc123.json",
711
+ is_deferred=False,
712
+ generate_notes=True,
713
+ skip_material_creation=True,
714
+ existing_material_id="mat_existing_456",
715
+ callback_url="https://api.example.com/noNotesGen/callback",
716
+ callback_api_key="secret",
717
+ )
718
+ ```
719
+
712
720
  ### Document Notes Fields
713
721
 
714
722
  | Field | Type | Required | Description |
@@ -716,12 +724,16 @@ result = client.document_notes.send(
716
724
  | `uploaded_file_id` | `str` | Yes | ID of the uploaded file record |
717
725
  | `study_set_id` | `str` | Yes | Study set to persist materials into |
718
726
  | `user_id` | `str` | Yes | User who uploaded the document |
719
- | `stash_key` | `str` | Yes | GCS key of the clustering stash JSON |
727
+ | `stash_key` | `str` | Yes | GCS key of the clustering stash JSON (deterministic: `clustering-stash/{study_set_id}/{uploaded_file_id}.json`) |
720
728
  | `is_deferred` | `bool` | No | Whether the study set has existing topics requiring approval (default: `False`) |
721
729
  | `generate_notes` | `bool` | No | Whether to generate notes (default: `True`). Set to `False` for noNotes materials |
722
730
  | `callback_url` | `Optional[str]` | No | URL to POST results to after completion |
723
731
  | `callback_api_key` | `Optional[str]` | No | Auth key for the callback |
724
732
  | `folder_id` | `Optional[str]` | No | Pre-created folder ID for the materials |
733
+ | `skip_material_creation` | `Optional[bool]` | No | Skip creating new materials; update an existing one instead (used for note regeneration) |
734
+ | `existing_material_id` | `Optional[str]` | No | ID of the existing material to update with generated notes (used with `skip_material_creation`) |
735
+ | `target_topic_index` | `Optional[int]` | No | Index of the specific topic to regenerate notes for (matches `material.order`). Omit to generate all topics. |
736
+ | `append_mode` | `Optional[bool]` | No | When `True`, generated notes are appended to existing material content rather than replacing it |
725
737
 
726
738
  ### Response Types
727
739
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sf-queue-sdk"
3
- version = "0.3.1b255"
3
+ version = "0.4.0"
4
4
  description = "Python SDK for sf-queue - Redis-based queue system"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """sf-queue Python SDK - Redis-based queue system."""
2
2
 
3
- __version__ = "0.3.1b255"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  from queue_sdk.client import QueueClient
6
6
  from queue_sdk.query import QueryClient
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
24
24
 
25
25
 
26
26
 
27
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1aqueue/email/v1/email.proto\x12\x0equeue.email.v1\"5\n\x0b\x45mailButton\x12\x12\n\x04text\x18\x01 \x01(\tR\x04text\x12\x12\n\x04href\x18\x02 \x01(\tR\x04href\"^\n\nEmailImage\x12\x10\n\x03src\x18\x01 \x01(\tR\x03src\x12\x10\n\x03\x61lt\x18\x02 \x01(\tR\x03\x61lt\x12\x14\n\x05width\x18\x03 \x01(\x05R\x05width\x12\x16\n\x06height\x18\x04 \x01(\x05R\x06height\"p\n\nEmailBrand\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n\x08logo_url\x18\x02 \x01(\tR\x07logoUrl\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12!\n\x0c\x61\x63\x63\x65nt_color\x18\x04 \x01(\tR\x0b\x61\x63\x63\x65ntColor\"\xa3\x03\n\x0c\x45mailRequest\x12\x0e\n\x02to\x18\x01 \x01(\tR\x02to\x12\x18\n\x07preview\x18\x02 \x01(\tR\x07preview\x12\x18\n\x07subject\x18\x03 \x01(\tR\x07subject\x12\x1e\n\nparagraphs\x18\x04 \x03(\tR\nparagraphs\x12\x33\n\x06\x62utton\x18\x05 \x01(\x0b\x32\x1b.queue.email.v1.EmailButtonR\x06\x62utton\x12\x19\n\x08reply_to\x18\x06 \x01(\tR\x07replyTo\x12\x30\n\x05image\x18\x07 \x01(\x0b\x32\x1a.queue.email.v1.EmailImageR\x05image\x12\x18\n\x07service\x18\x08 \x01(\tR\x07service\x12\x1a\n\x08template\x18\t \x01(\tR\x08template\x12\x12\n\x04html\x18\n \x01(\tR\x04html\x12\x12\n\x04\x66rom\x18\x0b \x01(\tR\x04\x66rom\x12\x1d\n\nresend_key\x18\x0c \x01(\tR\tresendKey\x12\x30\n\x05\x62rand\x18\r \x01(\x0b\x32\x1a.queue.email.v1.EmailBrandR\x05\x62rand\"\xa8\x03\n\x11\x42\x61tchEmailRequest\x12\x0e\n\x02to\x18\x01 \x03(\tR\x02to\x12\x18\n\x07preview\x18\x02 \x01(\tR\x07preview\x12\x18\n\x07subject\x18\x03 \x01(\tR\x07subject\x12\x1e\n\nparagraphs\x18\x04 \x03(\tR\nparagraphs\x12\x33\n\x06\x62utton\x18\x05 \x01(\x0b\x32\x1b.queue.email.v1.EmailButtonR\x06\x62utton\x12\x19\n\x08reply_to\x18\x06 \x01(\tR\x07replyTo\x12\x30\n\x05image\x18\x07 \x01(\x0b\x32\x1a.queue.email.v1.EmailImageR\x05image\x12\x18\n\x07service\x18\x08 \x01(\tR\x07service\x12\x1a\n\x08template\x18\t \x01(\tR\x08template\x12\x12\n\x04html\x18\n \x01(\tR\x04html\x12\x12\n\x04\x66rom\x18\x0b \x01(\tR\x04\x66rom\x12\x1d\n\nresend_key\x18\x0c \x01(\tR\tresendKey\x12\x30\n\x05\x62rand\x18\r \x01(\x0b\x32\x1a.queue.email.v1.EmailBrandR\x05\x62rand\"^\n\rEmailResponse\x12\x18\n\x07success\x18\x01 \x01(\x08R\x07success\x12\x1d\n\nmessage_id\x18\x02 \x01(\tR\tmessageId\x12\x14\n\x05\x65rror\x18\x03 \x01(\tR\x05\x65rrorBLZJgithub.com/StudyFetch/sf-queue/packages/go/proto-go/queue/email/v1;emailv1b\x06proto3')
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1aqueue/email/v1/email.proto\x12\x0equeue.email.v1\"5\n\x0b\x45mailButton\x12\x12\n\x04text\x18\x01 \x01(\tR\x04text\x12\x12\n\x04href\x18\x02 \x01(\tR\x04href\"^\n\nEmailImage\x12\x10\n\x03src\x18\x01 \x01(\tR\x03src\x12\x10\n\x03\x61lt\x18\x02 \x01(\tR\x03\x61lt\x12\x14\n\x05width\x18\x03 \x01(\x05R\x05width\x12\x16\n\x06height\x18\x04 \x01(\x05R\x06height\"p\n\nEmailBrand\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x19\n\x08logo_url\x18\x02 \x01(\tR\x07logoUrl\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12!\n\x0c\x61\x63\x63\x65nt_color\x18\x04 \x01(\tR\x0b\x61\x63\x63\x65ntColor\"\xc0\x03\n\x0c\x45mailRequest\x12\x0e\n\x02to\x18\x01 \x01(\tR\x02to\x12\x18\n\x07preview\x18\x02 \x01(\tR\x07preview\x12\x18\n\x07subject\x18\x03 \x01(\tR\x07subject\x12\x1e\n\nparagraphs\x18\x04 \x03(\tR\nparagraphs\x12\x33\n\x06\x62utton\x18\x05 \x01(\x0b\x32\x1b.queue.email.v1.EmailButtonR\x06\x62utton\x12\x19\n\x08reply_to\x18\x06 \x01(\tR\x07replyTo\x12\x30\n\x05image\x18\x07 \x01(\x0b\x32\x1a.queue.email.v1.EmailImageR\x05image\x12\x18\n\x07service\x18\x08 \x01(\tR\x07service\x12\x1a\n\x08template\x18\t \x01(\tR\x08template\x12\x12\n\x04html\x18\n \x01(\tR\x04html\x12\x12\n\x04\x66rom\x18\x0b \x01(\tR\x04\x66rom\x12\x1d\n\nresend_key\x18\x0c \x01(\tR\tresendKey\x12\x30\n\x05\x62rand\x18\r \x01(\x0b\x32\x1a.queue.email.v1.EmailBrandR\x05\x62rand\x12\x1b\n\tbody_html\x18\x0e \x01(\tR\x08\x62odyHtml\"\xc5\x03\n\x11\x42\x61tchEmailRequest\x12\x0e\n\x02to\x18\x01 \x03(\tR\x02to\x12\x18\n\x07preview\x18\x02 \x01(\tR\x07preview\x12\x18\n\x07subject\x18\x03 \x01(\tR\x07subject\x12\x1e\n\nparagraphs\x18\x04 \x03(\tR\nparagraphs\x12\x33\n\x06\x62utton\x18\x05 \x01(\x0b\x32\x1b.queue.email.v1.EmailButtonR\x06\x62utton\x12\x19\n\x08reply_to\x18\x06 \x01(\tR\x07replyTo\x12\x30\n\x05image\x18\x07 \x01(\x0b\x32\x1a.queue.email.v1.EmailImageR\x05image\x12\x18\n\x07service\x18\x08 \x01(\tR\x07service\x12\x1a\n\x08template\x18\t \x01(\tR\x08template\x12\x12\n\x04html\x18\n \x01(\tR\x04html\x12\x12\n\x04\x66rom\x18\x0b \x01(\tR\x04\x66rom\x12\x1d\n\nresend_key\x18\x0c \x01(\tR\tresendKey\x12\x30\n\x05\x62rand\x18\r \x01(\x0b\x32\x1a.queue.email.v1.EmailBrandR\x05\x62rand\x12\x1b\n\tbody_html\x18\x0e \x01(\tR\x08\x62odyHtml\"^\n\rEmailResponse\x12\x18\n\x07success\x18\x01 \x01(\x08R\x07success\x12\x1d\n\nmessage_id\x18\x02 \x01(\tR\tmessageId\x12\x14\n\x05\x65rror\x18\x03 \x01(\tR\x05\x65rrorBLZJgithub.com/StudyFetch/sf-queue/packages/go/proto-go/queue/email/v1;emailv1b\x06proto3')
28
28
 
29
29
  _globals = globals()
30
30
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -39,9 +39,9 @@ if not _descriptor._USE_C_DESCRIPTORS:
39
39
  _globals['_EMAILBRAND']._serialized_start=197
40
40
  _globals['_EMAILBRAND']._serialized_end=309
41
41
  _globals['_EMAILREQUEST']._serialized_start=312
42
- _globals['_EMAILREQUEST']._serialized_end=731
43
- _globals['_BATCHEMAILREQUEST']._serialized_start=734
44
- _globals['_BATCHEMAILREQUEST']._serialized_end=1158
45
- _globals['_EMAILRESPONSE']._serialized_start=1160
46
- _globals['_EMAILRESPONSE']._serialized_end=1254
42
+ _globals['_EMAILREQUEST']._serialized_end=760
43
+ _globals['_BATCHEMAILREQUEST']._serialized_start=763
44
+ _globals['_BATCHEMAILREQUEST']._serialized_end=1216
45
+ _globals['_EMAILRESPONSE']._serialized_start=1218
46
+ _globals['_EMAILRESPONSE']._serialized_end=1312
47
47
  # @@protoc_insertion_point(module_scope)
@@ -39,7 +39,7 @@ class EmailBrand(_message.Message):
39
39
  def __init__(self, name: _Optional[str] = ..., logo_url: _Optional[str] = ..., url: _Optional[str] = ..., accent_color: _Optional[str] = ...) -> None: ...
40
40
 
41
41
  class EmailRequest(_message.Message):
42
- __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template", "html", "resend_key", "brand")
42
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template", "html", "resend_key", "brand", "body_html")
43
43
  TO_FIELD_NUMBER: _ClassVar[int]
44
44
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
45
45
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -53,6 +53,7 @@ class EmailRequest(_message.Message):
53
53
  FROM_FIELD_NUMBER: _ClassVar[int]
54
54
  RESEND_KEY_FIELD_NUMBER: _ClassVar[int]
55
55
  BRAND_FIELD_NUMBER: _ClassVar[int]
56
+ BODY_HTML_FIELD_NUMBER: _ClassVar[int]
56
57
  to: str
57
58
  preview: str
58
59
  subject: str
@@ -65,10 +66,11 @@ class EmailRequest(_message.Message):
65
66
  html: str
66
67
  resend_key: str
67
68
  brand: EmailBrand
68
- def __init__(self, to: _Optional[str] = ..., preview: _Optional[str] = ..., subject: _Optional[str] = ..., paragraphs: _Optional[_Iterable[str]] = ..., button: _Optional[_Union[EmailButton, _Mapping]] = ..., reply_to: _Optional[str] = ..., image: _Optional[_Union[EmailImage, _Mapping]] = ..., service: _Optional[str] = ..., template: _Optional[str] = ..., html: _Optional[str] = ..., resend_key: _Optional[str] = ..., brand: _Optional[_Union[EmailBrand, _Mapping]] = ..., **kwargs) -> None: ...
69
+ body_html: str
70
+ def __init__(self, to: _Optional[str] = ..., preview: _Optional[str] = ..., subject: _Optional[str] = ..., paragraphs: _Optional[_Iterable[str]] = ..., button: _Optional[_Union[EmailButton, _Mapping]] = ..., reply_to: _Optional[str] = ..., image: _Optional[_Union[EmailImage, _Mapping]] = ..., service: _Optional[str] = ..., template: _Optional[str] = ..., html: _Optional[str] = ..., resend_key: _Optional[str] = ..., brand: _Optional[_Union[EmailBrand, _Mapping]] = ..., body_html: _Optional[str] = ..., **kwargs) -> None: ...
69
71
 
70
72
  class BatchEmailRequest(_message.Message):
71
- __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template", "html", "resend_key", "brand")
73
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template", "html", "resend_key", "brand", "body_html")
72
74
  TO_FIELD_NUMBER: _ClassVar[int]
73
75
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
74
76
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -82,6 +84,7 @@ class BatchEmailRequest(_message.Message):
82
84
  FROM_FIELD_NUMBER: _ClassVar[int]
83
85
  RESEND_KEY_FIELD_NUMBER: _ClassVar[int]
84
86
  BRAND_FIELD_NUMBER: _ClassVar[int]
87
+ BODY_HTML_FIELD_NUMBER: _ClassVar[int]
85
88
  to: _containers.RepeatedScalarFieldContainer[str]
86
89
  preview: str
87
90
  subject: str
@@ -94,7 +97,8 @@ class BatchEmailRequest(_message.Message):
94
97
  html: str
95
98
  resend_key: str
96
99
  brand: EmailBrand
97
- def __init__(self, to: _Optional[_Iterable[str]] = ..., preview: _Optional[str] = ..., subject: _Optional[str] = ..., paragraphs: _Optional[_Iterable[str]] = ..., button: _Optional[_Union[EmailButton, _Mapping]] = ..., reply_to: _Optional[str] = ..., image: _Optional[_Union[EmailImage, _Mapping]] = ..., service: _Optional[str] = ..., template: _Optional[str] = ..., html: _Optional[str] = ..., resend_key: _Optional[str] = ..., brand: _Optional[_Union[EmailBrand, _Mapping]] = ..., **kwargs) -> None: ...
100
+ body_html: str
101
+ def __init__(self, to: _Optional[_Iterable[str]] = ..., preview: _Optional[str] = ..., subject: _Optional[str] = ..., paragraphs: _Optional[_Iterable[str]] = ..., button: _Optional[_Union[EmailButton, _Mapping]] = ..., reply_to: _Optional[str] = ..., image: _Optional[_Union[EmailImage, _Mapping]] = ..., service: _Optional[str] = ..., template: _Optional[str] = ..., html: _Optional[str] = ..., resend_key: _Optional[str] = ..., brand: _Optional[_Union[EmailBrand, _Mapping]] = ..., body_html: _Optional[str] = ..., **kwargs) -> None: ...
98
102
 
99
103
  class EmailResponse(_message.Message):
100
104
  __slots__ = ("success", "message_id", "error")
@@ -30,6 +30,10 @@ class DocumentNotesQueue(BaseQueue):
30
30
  callback_url: Optional[str] = None,
31
31
  callback_api_key: Optional[str] = None,
32
32
  folder_id: Optional[str] = None,
33
+ skip_material_creation: Optional[bool] = None,
34
+ existing_material_id: Optional[str] = None,
35
+ target_topic_index: Optional[int] = None,
36
+ append_mode: Optional[bool] = None,
33
37
  ) -> SendResult:
34
38
  """Enqueue a note generation request."""
35
39
  self._validate(uploaded_file_id, study_set_id, user_id, stash_key)
@@ -43,6 +47,10 @@ class DocumentNotesQueue(BaseQueue):
43
47
  callback_url=callback_url,
44
48
  callback_api_key=callback_api_key,
45
49
  folder_id=folder_id,
50
+ skip_material_creation=skip_material_creation,
51
+ existing_material_id=existing_material_id,
52
+ target_topic_index=target_topic_index,
53
+ append_mode=append_mode,
46
54
  )
47
55
  message_id = self._enqueue(payload)
48
56
 
@@ -59,6 +67,10 @@ class DocumentNotesQueue(BaseQueue):
59
67
  callback_url: Optional[str],
60
68
  callback_api_key: Optional[str],
61
69
  folder_id: Optional[str],
70
+ skip_material_creation: Optional[bool],
71
+ existing_material_id: Optional[str],
72
+ target_topic_index: Optional[int],
73
+ append_mode: Optional[bool],
62
74
  ) -> dict[str, Any]:
63
75
  payload: dict[str, Any] = {
64
76
  "uploadedFileId": uploaded_file_id,
@@ -75,6 +87,14 @@ class DocumentNotesQueue(BaseQueue):
75
87
  payload["callbackApiKey"] = callback_api_key
76
88
  if folder_id is not None:
77
89
  payload["folderId"] = folder_id
90
+ if skip_material_creation is not None:
91
+ payload["skipMaterialCreation"] = skip_material_creation
92
+ if existing_material_id is not None:
93
+ payload["existingMaterialId"] = existing_material_id
94
+ if target_topic_index is not None:
95
+ payload["targetTopicIndex"] = target_topic_index
96
+ if append_mode is not None:
97
+ payload["appendMode"] = append_mode
78
98
 
79
99
  return payload
80
100
 
@@ -51,6 +51,7 @@ class DocumentUploadQueue(BaseQueue):
51
51
  callback_api_key: Optional[str] = None,
52
52
  study_plan_hook: Optional[bool] = None,
53
53
  skip_material_creation: Optional[bool] = None,
54
+ existing_material_id: Optional[str] = None,
54
55
  ) -> SendResult:
55
56
  """Enqueue a document upload processing request (fire and forget)."""
56
57
  self._validate(uploaded_file_id, study_set_id, user_id, pdf_s3_key, filename)
@@ -86,6 +87,7 @@ class DocumentUploadQueue(BaseQueue):
86
87
  callback_api_key=callback_api_key,
87
88
  study_plan_hook=study_plan_hook,
88
89
  skip_material_creation=skip_material_creation,
90
+ existing_material_id=existing_material_id,
89
91
  )
90
92
  message_id = self._enqueue(payload)
91
93
  self._set_initial_status(message_id)
@@ -171,6 +173,7 @@ class DocumentUploadQueue(BaseQueue):
171
173
  callback_api_key: Optional[str],
172
174
  study_plan_hook: Optional[bool],
173
175
  skip_material_creation: Optional[bool],
176
+ existing_material_id: Optional[str],
174
177
  ) -> dict[str, Any]:
175
178
  """Build the document upload payload (snake_case -> camelCase)."""
176
179
  payload: dict[str, Any] = {
@@ -222,6 +225,8 @@ class DocumentUploadQueue(BaseQueue):
222
225
  payload["studyPlanHook"] = study_plan_hook
223
226
  if skip_material_creation is not None:
224
227
  payload["skipMaterialCreation"] = skip_material_creation
228
+ if existing_material_id is not None:
229
+ payload["existingMaterialId"] = existing_material_id
225
230
 
226
231
  return payload
227
232
 
@@ -24,7 +24,7 @@ class EmailQueue(BaseQueue):
24
24
  to: str,
25
25
  preview: str,
26
26
  subject: str,
27
- paragraphs: list[str],
27
+ paragraphs: Optional[list[str]] = None,
28
28
  button: Optional[dict[str, str]] = None,
29
29
  reply_to: Optional[str] = None,
30
30
  image: Optional[dict[str, Any]] = None,
@@ -32,10 +32,22 @@ class EmailQueue(BaseQueue):
32
32
  template: Optional[str] = None,
33
33
  html: Optional[str] = None,
34
34
  brand: Optional[dict[str, str]] = None,
35
+ body_html: Optional[str] = None,
35
36
  ) -> SendResult:
36
37
  """Enqueue an email for processing (fire and forget)."""
37
38
  payload = self._build_payload(
38
- to, preview, subject, paragraphs, button, reply_to, image, service, template, html, brand
39
+ to,
40
+ preview,
41
+ subject,
42
+ paragraphs,
43
+ button,
44
+ reply_to,
45
+ image,
46
+ service,
47
+ template,
48
+ html,
49
+ brand,
50
+ body_html,
39
51
  )
40
52
  message_id = self._enqueue(payload)
41
53
  return SendResult(message_id=message_id)
@@ -45,7 +57,7 @@ class EmailQueue(BaseQueue):
45
57
  to: str,
46
58
  preview: str,
47
59
  subject: str,
48
- paragraphs: list[str],
60
+ paragraphs: Optional[list[str]] = None,
49
61
  button: Optional[dict[str, str]] = None,
50
62
  reply_to: Optional[str] = None,
51
63
  image: Optional[dict[str, Any]] = None,
@@ -54,10 +66,22 @@ class EmailQueue(BaseQueue):
54
66
  template: Optional[str] = None,
55
67
  html: Optional[str] = None,
56
68
  brand: Optional[dict[str, str]] = None,
69
+ body_html: Optional[str] = None,
57
70
  ) -> EmailResponse:
58
71
  """Enqueue an email and wait for processing confirmation."""
59
72
  payload = self._build_payload(
60
- to, preview, subject, paragraphs, button, reply_to, image, service, template, html, brand
73
+ to,
74
+ preview,
75
+ subject,
76
+ paragraphs,
77
+ button,
78
+ reply_to,
79
+ image,
80
+ service,
81
+ template,
82
+ html,
83
+ brand,
84
+ body_html,
61
85
  )
62
86
  return self._enqueue_and_wait(payload, timeout)
63
87
 
@@ -66,7 +90,7 @@ class EmailQueue(BaseQueue):
66
90
  to: list[str],
67
91
  preview: str,
68
92
  subject: str,
69
- paragraphs: list[str],
93
+ paragraphs: Optional[list[str]] = None,
70
94
  button: Optional[dict[str, str]] = None,
71
95
  reply_to: Optional[str] = None,
72
96
  image: Optional[dict[str, Any]] = None,
@@ -74,11 +98,23 @@ class EmailQueue(BaseQueue):
74
98
  template: Optional[str] = None,
75
99
  html: Optional[str] = None,
76
100
  brand: Optional[dict[str, str]] = None,
101
+ body_html: Optional[str] = None,
77
102
  ) -> SendResult:
78
103
  """Enqueue a batch email for processing (fire and forget)."""
79
104
  self._validate_batch(to)
80
105
  payload = self._build_batch_payload(
81
- to, preview, subject, paragraphs, button, reply_to, image, service, template, html, brand
106
+ to,
107
+ preview,
108
+ subject,
109
+ paragraphs,
110
+ button,
111
+ reply_to,
112
+ image,
113
+ service,
114
+ template,
115
+ html,
116
+ brand,
117
+ body_html,
82
118
  )
83
119
  message_id = self._enqueue(payload)
84
120
  return SendResult(message_id=message_id)
@@ -88,7 +124,7 @@ class EmailQueue(BaseQueue):
88
124
  to: list[str],
89
125
  preview: str,
90
126
  subject: str,
91
- paragraphs: list[str],
127
+ paragraphs: Optional[list[str]] = None,
92
128
  button: Optional[dict[str, str]] = None,
93
129
  reply_to: Optional[str] = None,
94
130
  image: Optional[dict[str, Any]] = None,
@@ -97,11 +133,23 @@ class EmailQueue(BaseQueue):
97
133
  template: Optional[str] = None,
98
134
  html: Optional[str] = None,
99
135
  brand: Optional[dict[str, str]] = None,
136
+ body_html: Optional[str] = None,
100
137
  ) -> BatchEmailResponse:
101
138
  """Enqueue a batch email and wait for processing confirmation."""
102
139
  self._validate_batch(to)
103
140
  payload = self._build_batch_payload(
104
- to, preview, subject, paragraphs, button, reply_to, image, service, template, html, brand
141
+ to,
142
+ preview,
143
+ subject,
144
+ paragraphs,
145
+ button,
146
+ reply_to,
147
+ image,
148
+ service,
149
+ template,
150
+ html,
151
+ brand,
152
+ body_html,
105
153
  )
106
154
  response = self._enqueue_and_wait(payload, timeout)
107
155
 
@@ -120,7 +168,7 @@ class EmailQueue(BaseQueue):
120
168
  to: str,
121
169
  preview: str,
122
170
  subject: str,
123
- paragraphs: list[str],
171
+ paragraphs: Optional[list[str]],
124
172
  button: Optional[dict[str, str]],
125
173
  reply_to: Optional[str],
126
174
  image: Optional[dict[str, Any]],
@@ -128,13 +176,14 @@ class EmailQueue(BaseQueue):
128
176
  template: Optional[str] = None,
129
177
  html: Optional[str] = None,
130
178
  brand: Optional[dict[str, str]] = None,
179
+ body_html: Optional[str] = None,
131
180
  ) -> dict[str, Any]:
132
181
  """Build and sanitize the email payload."""
133
182
  payload: dict[str, Any] = {
134
183
  "to": to,
135
184
  "preview": sanitize(preview),
136
185
  "subject": sanitize(subject),
137
- "paragraphs": sanitize_list(paragraphs),
186
+ "paragraphs": sanitize_list(paragraphs or []),
138
187
  }
139
188
 
140
189
  if button:
@@ -163,6 +212,9 @@ class EmailQueue(BaseQueue):
163
212
  if html:
164
213
  payload["html"] = html
165
214
 
215
+ if body_html:
216
+ payload["bodyHtml"] = body_html
217
+
166
218
  if brand:
167
219
  payload["brand"] = self._build_brand(brand)
168
220
 
@@ -170,7 +222,7 @@ class EmailQueue(BaseQueue):
170
222
 
171
223
  @staticmethod
172
224
  def _build_brand(brand: dict[str, str]) -> dict[str, str]:
173
- """Sanitize whitelabel branding. Name is user text; URLs/color pass through."""
225
+ """Sanitize Honen template branding."""
174
226
  result: dict[str, str] = {}
175
227
  if brand.get("name"):
176
228
  result["name"] = sanitize(brand["name"])
@@ -187,7 +239,7 @@ class EmailQueue(BaseQueue):
187
239
  to: list[str],
188
240
  preview: str,
189
241
  subject: str,
190
- paragraphs: list[str],
242
+ paragraphs: Optional[list[str]],
191
243
  button: Optional[dict[str, str]],
192
244
  reply_to: Optional[str],
193
245
  image: Optional[dict[str, Any]],
@@ -195,6 +247,7 @@ class EmailQueue(BaseQueue):
195
247
  template: Optional[str] = None,
196
248
  html: Optional[str] = None,
197
249
  brand: Optional[dict[str, str]] = None,
250
+ body_html: Optional[str] = None,
198
251
  ) -> dict[str, Any]:
199
252
  """Build and sanitize the batch email payload."""
200
253
  payload = self._build_payload(
@@ -209,6 +262,7 @@ class EmailQueue(BaseQueue):
209
262
  template=template,
210
263
  html=html,
211
264
  brand=brand,
265
+ body_html=body_html,
212
266
  )
213
267
  payload["to"] = to
214
268
  payload["batch"] = True
@@ -281,6 +281,9 @@ class DocumentNotesData:
281
281
  callback_url: Optional[str] = None
282
282
  callback_api_key: Optional[str] = None
283
283
  folder_id: Optional[str] = None
284
+ skip_material_creation: Optional[bool] = None
285
+ existing_material_id: Optional[str] = None
286
+ target_topic_index: Optional[int] = None
284
287
 
285
288
 
286
289
  @dataclass