sf-queue-sdk 0.2.0b23__tar.gz → 0.2.0b25__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 (25) hide show
  1. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/PKG-INFO +79 -1
  2. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/README.md +78 -0
  3. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/pyproject.toml +1 -1
  4. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/__init__.py +7 -1
  5. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/client.py +2 -0
  6. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/email/v1/email_pb2.py +10 -10
  7. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +12 -4
  8. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.py +4 -4
  9. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.py +4 -4
  10. sf_queue_sdk-0.2.0b25/queue_sdk/query.py +22 -0
  11. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/email_queue.py +33 -5
  12. sf_queue_sdk-0.2.0b25/queue_sdk/queues/recommendation_queue.py +136 -0
  13. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/types.py +24 -0
  14. sf_queue_sdk-0.2.0b23/queue_sdk/queues/recommendation_queue.py +0 -72
  15. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/.gitignore +0 -0
  16. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/__init__.py +0 -0
  17. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.pyi +0 -0
  18. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.pyi +0 -0
  19. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/__init__.py +0 -0
  20. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/base_queue.py +0 -0
  21. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/pdf_to_image_queue.py +0 -0
  22. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/tool_assignment_queue.py +0 -0
  23. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/queues/topic_assignment_queue.py +0 -0
  24. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/redis_client.py +0 -0
  25. {sf_queue_sdk-0.2.0b23 → sf_queue_sdk-0.2.0b25}/queue_sdk/sanitize.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sf-queue-sdk
3
- Version: 0.2.0b23
3
+ Version: 0.2.0b25
4
4
  Summary: Python SDK for sf-queue - Redis-based queue system
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: redis>=5.0.0
@@ -150,6 +150,8 @@ print(result.error) # error message if all failed
150
150
  | `button` | `{"text": str, "href": str}` | No | Call-to-action button |
151
151
  | `reply_to` | `str` | No | Reply-to email address |
152
152
  | `image` | `{"src": str, "alt"?: str, "width"?: int, "height"?: int}` | No | Image in email body |
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. |
154
+ | `template` | `str` | No | Template name override (e.g. `"sparkles"`). Overrides the service's default template. Falls back to `"studyfetch"` when omitted. |
153
155
 
154
156
  ## Optional Fields
155
157
 
@@ -170,6 +172,31 @@ client.email.send(
170
172
  )
171
173
  ```
172
174
 
175
+ ## Multi-Service Routing
176
+
177
+ Use `service` to route emails through a different service's credentials and branding. The consumer resolves the service name to its configured Resend API key, from address, and default template. Use `template` to override the template for a specific email.
178
+
179
+ ```python
180
+ # Send via a different service's email account and template
181
+ client.email.send(
182
+ to="user@example.com",
183
+ preview="Welcome!",
184
+ subject="Welcome!",
185
+ paragraphs=["Welcome to the platform!"],
186
+ service="sparkles",
187
+ )
188
+
189
+ # Override the template for a specific email
190
+ client.email.send(
191
+ to="user@example.com",
192
+ preview="Welcome!",
193
+ subject="Welcome!",
194
+ paragraphs=["Welcome to the platform!"],
195
+ service="sparkles",
196
+ template="sparkles",
197
+ )
198
+ ```
199
+
173
200
  ## Migrating from sendEmail / sendBatchEmail
174
201
 
175
202
  The SDK is a drop-in replacement. Field names match the existing functions:
@@ -384,6 +411,32 @@ print(result.success)
384
411
  print(result.message_id)
385
412
  ```
386
413
 
414
+ ### Batch (fire and forget)
415
+
416
+ ```python
417
+ result = client.recommendations.create_batch([
418
+ {"user_id": "user_1", "topic_id": "topic_1", "limit": 10},
419
+ {"user_id": "user_2", "topic_id": "topic_2"},
420
+ ])
421
+ print("Enqueued:", result.message_id)
422
+ ```
423
+
424
+ ### Batch (wait for confirmation)
425
+
426
+ ```python
427
+ result = client.recommendations.create_batch_and_wait(
428
+ [
429
+ {"user_id": "user_1", "topic_id": "topic_1"},
430
+ {"user_id": "user_2", "topic_id": "topic_2"},
431
+ ],
432
+ timeout=60,
433
+ )
434
+ print(result.success)
435
+ print(result.total)
436
+ print(result.successful)
437
+ print(result.failed)
438
+ ```
439
+
387
440
  ### Recommendation Fields
388
441
 
389
442
  | Field | Type | Required | Description |
@@ -392,6 +445,31 @@ print(result.message_id)
392
445
  | `topic_id` | `str` | Yes | Topic context for recommendations |
393
446
  | `limit` | `Optional[int]` | No | Max recommendations to return |
394
447
 
448
+ ## Query (Direct Key Lookups)
449
+
450
+ Read values directly from Redis by key. These are plain `GET` / `MGET` calls, not queue operations.
451
+
452
+ ### Get a single key
453
+
454
+ ```python
455
+ value = client.query.get("mykey")
456
+ print(value) # str or None
457
+ ```
458
+
459
+ ### Get multiple keys
460
+
461
+ ```python
462
+ values = client.query.get_many(["key1", "key2", "key3"])
463
+ print(values) # list[str | None]
464
+ ```
465
+
466
+ ### Query Methods
467
+
468
+ | Method | Return Type | Description |
469
+ |--------|-------------|-------------|
470
+ | `get(key)` | `Optional[str]` | Fetch a single key via Redis GET |
471
+ | `get_many(keys)` | `list[Optional[str]]` | Fetch multiple keys via Redis MGET |
472
+
395
473
  ## Cleanup
396
474
 
397
475
  ```python
@@ -142,6 +142,8 @@ print(result.error) # error message if all failed
142
142
  | `button` | `{"text": str, "href": str}` | No | Call-to-action button |
143
143
  | `reply_to` | `str` | No | Reply-to email address |
144
144
  | `image` | `{"src": str, "alt"?: str, "width"?: int, "height"?: int}` | No | Image in email body |
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. |
146
+ | `template` | `str` | No | Template name override (e.g. `"sparkles"`). Overrides the service's default template. Falls back to `"studyfetch"` when omitted. |
145
147
 
146
148
  ## Optional Fields
147
149
 
@@ -162,6 +164,31 @@ client.email.send(
162
164
  )
163
165
  ```
164
166
 
167
+ ## Multi-Service Routing
168
+
169
+ Use `service` to route emails through a different service's credentials and branding. The consumer resolves the service name to its configured Resend API key, from address, and default template. Use `template` to override the template for a specific email.
170
+
171
+ ```python
172
+ # Send via a different service's email account and template
173
+ client.email.send(
174
+ to="user@example.com",
175
+ preview="Welcome!",
176
+ subject="Welcome!",
177
+ paragraphs=["Welcome to the platform!"],
178
+ service="sparkles",
179
+ )
180
+
181
+ # Override the template for a specific email
182
+ client.email.send(
183
+ to="user@example.com",
184
+ preview="Welcome!",
185
+ subject="Welcome!",
186
+ paragraphs=["Welcome to the platform!"],
187
+ service="sparkles",
188
+ template="sparkles",
189
+ )
190
+ ```
191
+
165
192
  ## Migrating from sendEmail / sendBatchEmail
166
193
 
167
194
  The SDK is a drop-in replacement. Field names match the existing functions:
@@ -376,6 +403,32 @@ print(result.success)
376
403
  print(result.message_id)
377
404
  ```
378
405
 
406
+ ### Batch (fire and forget)
407
+
408
+ ```python
409
+ result = client.recommendations.create_batch([
410
+ {"user_id": "user_1", "topic_id": "topic_1", "limit": 10},
411
+ {"user_id": "user_2", "topic_id": "topic_2"},
412
+ ])
413
+ print("Enqueued:", result.message_id)
414
+ ```
415
+
416
+ ### Batch (wait for confirmation)
417
+
418
+ ```python
419
+ result = client.recommendations.create_batch_and_wait(
420
+ [
421
+ {"user_id": "user_1", "topic_id": "topic_1"},
422
+ {"user_id": "user_2", "topic_id": "topic_2"},
423
+ ],
424
+ timeout=60,
425
+ )
426
+ print(result.success)
427
+ print(result.total)
428
+ print(result.successful)
429
+ print(result.failed)
430
+ ```
431
+
379
432
  ### Recommendation Fields
380
433
 
381
434
  | Field | Type | Required | Description |
@@ -384,6 +437,31 @@ print(result.message_id)
384
437
  | `topic_id` | `str` | Yes | Topic context for recommendations |
385
438
  | `limit` | `Optional[int]` | No | Max recommendations to return |
386
439
 
440
+ ## Query (Direct Key Lookups)
441
+
442
+ Read values directly from Redis by key. These are plain `GET` / `MGET` calls, not queue operations.
443
+
444
+ ### Get a single key
445
+
446
+ ```python
447
+ value = client.query.get("mykey")
448
+ print(value) # str or None
449
+ ```
450
+
451
+ ### Get multiple keys
452
+
453
+ ```python
454
+ values = client.query.get_many(["key1", "key2", "key3"])
455
+ print(values) # list[str | None]
456
+ ```
457
+
458
+ ### Query Methods
459
+
460
+ | Method | Return Type | Description |
461
+ |--------|-------------|-------------|
462
+ | `get(key)` | `Optional[str]` | Fetch a single key via Redis GET |
463
+ | `get_many(keys)` | `list[Optional[str]]` | Fetch multiple keys via Redis MGET |
464
+
387
465
  ## Cleanup
388
466
 
389
467
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sf-queue-sdk"
3
- version = "0.2.0b23"
3
+ version = "0.2.0b25"
4
4
  description = "Python SDK for sf-queue - Redis-based queue system"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,11 +1,14 @@
1
1
  """sf-queue Python SDK - Redis-based queue system."""
2
2
 
3
- __version__ = "0.2.0b23"
3
+ __version__ = "0.2.0b25"
4
4
 
5
5
  from queue_sdk.client import QueueClient
6
+ from queue_sdk.query import QueryClient
6
7
  from queue_sdk.types import (
7
8
  BatchEmailResponse,
8
9
  BatchQueueResponse,
10
+ BatchRecommendationRequestData,
11
+ BatchRecommendationResponse,
9
12
  BatchToolAssignmentData,
10
13
  BatchTopicAssignmentData,
11
14
  ConvertData,
@@ -25,6 +28,7 @@ from queue_sdk.types import (
25
28
 
26
29
  __all__ = [
27
30
  "QueueClient",
31
+ "QueryClient",
28
32
  "QueueClientConfig",
29
33
  "EmailData",
30
34
  "EmailButton",
@@ -35,6 +39,8 @@ __all__ = [
35
39
  "BatchQueueResponse",
36
40
  "RecommendationRequestData",
37
41
  "RecommendationResponse",
42
+ "BatchRecommendationRequestData",
43
+ "BatchRecommendationResponse",
38
44
  "TopicAssignmentData",
39
45
  "BatchTopicAssignmentData",
40
46
  "ToolAssignmentData",
@@ -1,5 +1,6 @@
1
1
  """QueueClient - main entry point for the Python SDK."""
2
2
 
3
+ from queue_sdk.query import QueryClient
3
4
  from queue_sdk.queues.email_queue import EmailQueue
4
5
  from queue_sdk.queues.pdf_to_image_queue import PdfToImageQueue
5
6
  from queue_sdk.queues.recommendation_queue import RecommendationQueue
@@ -67,6 +68,7 @@ class QueueClient:
67
68
  self.tool_assignment = ToolAssignmentQueue(self._redis, self._environment)
68
69
  self.pdf_to_image = PdfToImageQueue(self._redis, self._environment)
69
70
  self.recommendations = RecommendationQueue(self._redis, self._environment)
71
+ self.query = QueryClient(self._redis)
70
72
 
71
73
  def disconnect(self) -> None:
72
74
  """Close the Redis connection."""
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: queue/email/v1/email.proto
5
- # Protobuf Python Version: 6.33.5
5
+ # Protobuf Python Version: 7.34.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,9 +11,9 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 33,
16
- 5,
14
+ 7,
15
+ 34,
16
+ 0,
17
17
  '',
18
18
  'queue/email/v1/email.proto'
19
19
  )
@@ -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\"\xf4\x01\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\"\xf9\x01\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\"^\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\"\xaa\x02\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\"\xaf\x02\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\"^\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)
@@ -37,9 +37,9 @@ if not _descriptor._USE_C_DESCRIPTORS:
37
37
  _globals['_EMAILIMAGE']._serialized_start=101
38
38
  _globals['_EMAILIMAGE']._serialized_end=195
39
39
  _globals['_EMAILREQUEST']._serialized_start=198
40
- _globals['_EMAILREQUEST']._serialized_end=442
41
- _globals['_BATCHEMAILREQUEST']._serialized_start=445
42
- _globals['_BATCHEMAILREQUEST']._serialized_end=694
43
- _globals['_EMAILRESPONSE']._serialized_start=696
44
- _globals['_EMAILRESPONSE']._serialized_end=790
40
+ _globals['_EMAILREQUEST']._serialized_end=496
41
+ _globals['_BATCHEMAILREQUEST']._serialized_start=499
42
+ _globals['_BATCHEMAILREQUEST']._serialized_end=802
43
+ _globals['_EMAILRESPONSE']._serialized_start=804
44
+ _globals['_EMAILRESPONSE']._serialized_end=898
45
45
  # @@protoc_insertion_point(module_scope)
@@ -27,7 +27,7 @@ class EmailImage(_message.Message):
27
27
  def __init__(self, src: _Optional[str] = ..., alt: _Optional[str] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ...
28
28
 
29
29
  class EmailRequest(_message.Message):
30
- __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image")
30
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template")
31
31
  TO_FIELD_NUMBER: _ClassVar[int]
32
32
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
33
33
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -35,6 +35,8 @@ class EmailRequest(_message.Message):
35
35
  BUTTON_FIELD_NUMBER: _ClassVar[int]
36
36
  REPLY_TO_FIELD_NUMBER: _ClassVar[int]
37
37
  IMAGE_FIELD_NUMBER: _ClassVar[int]
38
+ SERVICE_FIELD_NUMBER: _ClassVar[int]
39
+ TEMPLATE_FIELD_NUMBER: _ClassVar[int]
38
40
  to: str
39
41
  preview: str
40
42
  subject: str
@@ -42,10 +44,12 @@ class EmailRequest(_message.Message):
42
44
  button: EmailButton
43
45
  reply_to: str
44
46
  image: EmailImage
45
- 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]] = ...) -> None: ...
47
+ service: str
48
+ template: str
49
+ 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] = ...) -> None: ...
46
50
 
47
51
  class BatchEmailRequest(_message.Message):
48
- __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image")
52
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template")
49
53
  TO_FIELD_NUMBER: _ClassVar[int]
50
54
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
51
55
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -53,6 +57,8 @@ class BatchEmailRequest(_message.Message):
53
57
  BUTTON_FIELD_NUMBER: _ClassVar[int]
54
58
  REPLY_TO_FIELD_NUMBER: _ClassVar[int]
55
59
  IMAGE_FIELD_NUMBER: _ClassVar[int]
60
+ SERVICE_FIELD_NUMBER: _ClassVar[int]
61
+ TEMPLATE_FIELD_NUMBER: _ClassVar[int]
56
62
  to: _containers.RepeatedScalarFieldContainer[str]
57
63
  preview: str
58
64
  subject: str
@@ -60,7 +66,9 @@ class BatchEmailRequest(_message.Message):
60
66
  button: EmailButton
61
67
  reply_to: str
62
68
  image: EmailImage
63
- 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]] = ...) -> None: ...
69
+ service: str
70
+ template: str
71
+ 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] = ...) -> None: ...
64
72
 
65
73
  class EmailResponse(_message.Message):
66
74
  __slots__ = ("success", "message_id", "error")
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: queue/tool_assignment/v1/tool_assignment.proto
5
- # Protobuf Python Version: 6.33.5
5
+ # Protobuf Python Version: 7.34.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,9 +11,9 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 33,
16
- 5,
14
+ 7,
15
+ 34,
16
+ 0,
17
17
  '',
18
18
  'queue/tool_assignment/v1/tool_assignment.proto'
19
19
  )
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: queue/topic_assignment/v1/topic_assignment.proto
5
- # Protobuf Python Version: 6.33.5
5
+ # Protobuf Python Version: 7.34.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,9 +11,9 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 33,
16
- 5,
14
+ 7,
15
+ 34,
16
+ 0,
17
17
  '',
18
18
  'queue/topic_assignment/v1/topic_assignment.proto'
19
19
  )
@@ -0,0 +1,22 @@
1
+ """QueryClient - direct Redis key lookups."""
2
+
3
+ from typing import Optional
4
+
5
+ import redis as redis_lib
6
+
7
+
8
+ class QueryClient:
9
+ """Direct Redis key lookups (GET, MGET)."""
10
+
11
+ def __init__(self, redis_client: redis_lib.Redis):
12
+ self._redis = redis_client
13
+
14
+ def get(self, key: str) -> Optional[str]:
15
+ """Fetch a single key from Redis."""
16
+ return self._redis.get(key)
17
+
18
+ def get_many(self, keys: list[str]) -> list[Optional[str]]:
19
+ """Fetch multiple keys from Redis in a single MGET call."""
20
+ if not keys:
21
+ return []
22
+ return self._redis.mget(*keys)
@@ -28,9 +28,13 @@ class EmailQueue(BaseQueue):
28
28
  button: Optional[dict[str, str]] = None,
29
29
  reply_to: Optional[str] = None,
30
30
  image: Optional[dict[str, Any]] = None,
31
+ service: Optional[str] = None,
32
+ template: Optional[str] = None,
31
33
  ) -> SendResult:
32
34
  """Enqueue an email for processing (fire and forget)."""
33
- payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
35
+ payload = self._build_payload(
36
+ to, preview, subject, paragraphs, button, reply_to, image, service, template
37
+ )
34
38
  message_id = self._enqueue(payload)
35
39
  return SendResult(message_id=message_id)
36
40
 
@@ -44,9 +48,13 @@ class EmailQueue(BaseQueue):
44
48
  reply_to: Optional[str] = None,
45
49
  image: Optional[dict[str, Any]] = None,
46
50
  timeout: Optional[int] = None,
51
+ service: Optional[str] = None,
52
+ template: Optional[str] = None,
47
53
  ) -> EmailResponse:
48
54
  """Enqueue an email and wait for processing confirmation."""
49
- payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
55
+ payload = self._build_payload(
56
+ to, preview, subject, paragraphs, button, reply_to, image, service, template
57
+ )
50
58
  return self._enqueue_and_wait(payload, timeout)
51
59
 
52
60
  def send_batch(
@@ -58,10 +66,14 @@ class EmailQueue(BaseQueue):
58
66
  button: Optional[dict[str, str]] = None,
59
67
  reply_to: Optional[str] = None,
60
68
  image: Optional[dict[str, Any]] = None,
69
+ service: Optional[str] = None,
70
+ template: Optional[str] = None,
61
71
  ) -> SendResult:
62
72
  """Enqueue a batch email for processing (fire and forget)."""
63
73
  self._validate_batch(to)
64
- payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
74
+ payload = self._build_batch_payload(
75
+ to, preview, subject, paragraphs, button, reply_to, image, service, template
76
+ )
65
77
  message_id = self._enqueue(payload)
66
78
  return SendResult(message_id=message_id)
67
79
 
@@ -75,10 +87,14 @@ class EmailQueue(BaseQueue):
75
87
  reply_to: Optional[str] = None,
76
88
  image: Optional[dict[str, Any]] = None,
77
89
  timeout: Optional[int] = None,
90
+ service: Optional[str] = None,
91
+ template: Optional[str] = None,
78
92
  ) -> BatchEmailResponse:
79
93
  """Enqueue a batch email and wait for processing confirmation."""
80
94
  self._validate_batch(to)
81
- payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
95
+ payload = self._build_batch_payload(
96
+ to, preview, subject, paragraphs, button, reply_to, image, service, template
97
+ )
82
98
  response = self._enqueue_and_wait(payload, timeout)
83
99
 
84
100
  return BatchEmailResponse(
@@ -100,6 +116,8 @@ class EmailQueue(BaseQueue):
100
116
  button: Optional[dict[str, str]],
101
117
  reply_to: Optional[str],
102
118
  image: Optional[dict[str, Any]],
119
+ service: Optional[str] = None,
120
+ template: Optional[str] = None,
103
121
  ) -> dict[str, Any]:
104
122
  """Build and sanitize the email payload."""
105
123
  payload: dict[str, Any] = {
@@ -126,6 +144,12 @@ class EmailQueue(BaseQueue):
126
144
  "height": image.get("height"),
127
145
  }
128
146
 
147
+ if service:
148
+ payload["service"] = service
149
+
150
+ if template:
151
+ payload["template"] = template
152
+
129
153
  return payload
130
154
 
131
155
  def _build_batch_payload(
@@ -137,16 +161,20 @@ class EmailQueue(BaseQueue):
137
161
  button: Optional[dict[str, str]],
138
162
  reply_to: Optional[str],
139
163
  image: Optional[dict[str, Any]],
164
+ service: Optional[str] = None,
165
+ template: Optional[str] = None,
140
166
  ) -> dict[str, Any]:
141
167
  """Build and sanitize the batch email payload."""
142
168
  payload = self._build_payload(
143
- to="", # not used for batch
169
+ to="",
144
170
  preview=preview,
145
171
  subject=subject,
146
172
  paragraphs=paragraphs,
147
173
  button=button,
148
174
  reply_to=reply_to,
149
175
  image=image,
176
+ service=service,
177
+ template=template,
150
178
  )
151
179
  payload["to"] = to
152
180
  payload["batch"] = True
@@ -0,0 +1,136 @@
1
+ """Recommendation queue with typed methods."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Optional
5
+
6
+ import redis as redis_lib
7
+
8
+ from queue_sdk.queues.base_queue import BaseQueue
9
+ from queue_sdk.types import (
10
+ BatchRecommendationResponse,
11
+ RecommendationResponse,
12
+ SendResult,
13
+ )
14
+
15
+
16
+ class RecommendationQueue(BaseQueue):
17
+ """Typed methods for creating recommendation requests via the queue."""
18
+
19
+ def __init__(self, redis_client: redis_lib.Redis, environment: str):
20
+ super().__init__(redis_client, environment, "recommendation-requests")
21
+
22
+ def create(
23
+ self,
24
+ user_id: str,
25
+ topic_id: str,
26
+ limit: Optional[int] = None,
27
+ ) -> SendResult:
28
+ """Enqueue a recommendation request (fire and forget)."""
29
+ self._validate(user_id, topic_id)
30
+ payload = self._build_payload(user_id, topic_id, limit)
31
+ message_id = self._enqueue(payload)
32
+ return SendResult(message_id=message_id)
33
+
34
+ def create_and_wait(
35
+ self,
36
+ user_id: str,
37
+ topic_id: str,
38
+ limit: Optional[int] = None,
39
+ timeout: Optional[int] = None,
40
+ ) -> RecommendationResponse:
41
+ """Enqueue a recommendation request and wait for the result."""
42
+ self._validate(user_id, topic_id)
43
+ payload = self._build_payload(user_id, topic_id, limit)
44
+ response = self._enqueue_and_wait(payload, timeout)
45
+
46
+ return RecommendationResponse(
47
+ success=response.success,
48
+ message_id=response.message_id,
49
+ error=response.error,
50
+ processed_at=response.processed_at,
51
+ )
52
+
53
+ def create_batch(
54
+ self,
55
+ requests: list[dict[str, Any]],
56
+ ) -> SendResult:
57
+ """Enqueue multiple recommendation requests (fire and forget)."""
58
+ self._validate_batch(requests)
59
+ payload = self._build_batch_payload(requests)
60
+ message_id = self._enqueue(payload)
61
+ return SendResult(message_id=message_id)
62
+
63
+ def create_batch_and_wait(
64
+ self,
65
+ requests: list[dict[str, Any]],
66
+ timeout: Optional[int] = None,
67
+ ) -> BatchRecommendationResponse:
68
+ """Enqueue multiple recommendation requests and wait for confirmation."""
69
+ self._validate_batch(requests)
70
+ payload = self._build_batch_payload(requests)
71
+ response = self._enqueue_and_wait(payload, timeout)
72
+
73
+ return BatchRecommendationResponse(
74
+ success=response.success,
75
+ message_id=response.message_id,
76
+ error=response.error,
77
+ processed_at=response.processed_at,
78
+ total=getattr(response, "total", len(requests)),
79
+ successful=getattr(
80
+ response, "successful", len(requests) if response.success else 0
81
+ ),
82
+ failed=getattr(response, "failed", 0 if response.success else len(requests)),
83
+ )
84
+
85
+ def _build_payload(
86
+ self,
87
+ user_id: str,
88
+ topic_id: str,
89
+ limit: Optional[int],
90
+ ) -> dict[str, Any]:
91
+ """Build the recommendation request payload."""
92
+ payload: dict[str, Any] = {
93
+ "userId": user_id,
94
+ "topicId": topic_id,
95
+ "requestedAt": datetime.now(timezone.utc).isoformat(),
96
+ "key": f"{user_id}-{topic_id}",
97
+ }
98
+ if limit is not None:
99
+ payload["limit"] = limit
100
+ return payload
101
+
102
+ def _build_batch_payload(
103
+ self, requests: list[dict[str, Any]]
104
+ ) -> dict[str, Any]:
105
+ """Build the batch recommendation request payload."""
106
+ return {
107
+ "batch": True,
108
+ "requests": [
109
+ self._build_payload(
110
+ r.get("user_id", ""),
111
+ r.get("topic_id", ""),
112
+ r.get("limit"),
113
+ )
114
+ for r in requests
115
+ ],
116
+ }
117
+
118
+ @staticmethod
119
+ def _validate(user_id: str, topic_id: str) -> None:
120
+ """Validate the recommendation request."""
121
+ if not user_id:
122
+ raise ValueError("user_id is required")
123
+ if not topic_id:
124
+ raise ValueError("topic_id is required")
125
+
126
+ def _validate_batch(self, requests: list[dict[str, Any]]) -> None:
127
+ """Validate the batch of recommendation requests."""
128
+ if len(requests) == 0:
129
+ raise ValueError("At least one request is required")
130
+ if len(requests) > 100:
131
+ raise ValueError("Maximum 100 requests allowed per batch")
132
+ for r in requests:
133
+ self._validate(
134
+ r.get("user_id", ""),
135
+ r.get("topic_id", ""),
136
+ )
@@ -42,6 +42,8 @@ class EmailData:
42
42
  button: Optional[EmailButton] = None
43
43
  reply_to: Optional[str] = None
44
44
  image: Optional[EmailImage] = None
45
+ service: Optional[str] = None
46
+ template: Optional[str] = None
45
47
 
46
48
 
47
49
  @dataclass
@@ -55,6 +57,8 @@ class BatchEmailData:
55
57
  button: Optional[EmailButton] = None
56
58
  reply_to: Optional[str] = None
57
59
  image: Optional[EmailImage] = None
60
+ service: Optional[str] = None
61
+ template: Optional[str] = None
58
62
 
59
63
 
60
64
  @dataclass
@@ -147,6 +151,26 @@ class RecommendationResponse:
147
151
  processed_at: Optional[str] = None
148
152
 
149
153
 
154
+ @dataclass
155
+ class BatchRecommendationRequestData:
156
+ """Data for multiple recommendation requests."""
157
+
158
+ requests: list[dict]
159
+
160
+
161
+ @dataclass
162
+ class BatchRecommendationResponse:
163
+ """Response from a processed batch of recommendation requests."""
164
+
165
+ success: bool
166
+ message_id: str
167
+ total: int
168
+ successful: int
169
+ failed: int
170
+ error: Optional[str] = None
171
+ processed_at: Optional[str] = None
172
+
173
+
150
174
  @dataclass
151
175
  class ConvertData:
152
176
  """Data for a PDF/video to image conversion request."""
@@ -1,72 +0,0 @@
1
- """Recommendation queue with typed methods."""
2
-
3
- from datetime import datetime, timezone
4
- from typing import Any, Optional
5
-
6
- import redis as redis_lib
7
-
8
- from queue_sdk.queues.base_queue import BaseQueue
9
- from queue_sdk.types import RecommendationResponse, SendResult
10
-
11
-
12
- class RecommendationQueue(BaseQueue):
13
- """Typed methods for creating recommendation requests via the queue."""
14
-
15
- def __init__(self, redis_client: redis_lib.Redis, environment: str):
16
- super().__init__(redis_client, environment, "recommendation-requests")
17
-
18
- def create(
19
- self,
20
- user_id: str,
21
- topic_id: str,
22
- limit: Optional[int] = None,
23
- ) -> SendResult:
24
- """Enqueue a recommendation request (fire and forget)."""
25
- self._validate(user_id, topic_id)
26
- payload = self._build_payload(user_id, topic_id, limit)
27
- message_id = self._enqueue(payload)
28
- return SendResult(message_id=message_id)
29
-
30
- def create_and_wait(
31
- self,
32
- user_id: str,
33
- topic_id: str,
34
- limit: Optional[int] = None,
35
- timeout: Optional[int] = None,
36
- ) -> RecommendationResponse:
37
- """Enqueue a recommendation request and wait for the result."""
38
- self._validate(user_id, topic_id)
39
- payload = self._build_payload(user_id, topic_id, limit)
40
- response = self._enqueue_and_wait(payload, timeout)
41
-
42
- return RecommendationResponse(
43
- success=response.success,
44
- message_id=response.message_id,
45
- error=response.error,
46
- processed_at=response.processed_at,
47
- )
48
-
49
- def _build_payload(
50
- self,
51
- user_id: str,
52
- topic_id: str,
53
- limit: Optional[int],
54
- ) -> dict[str, Any]:
55
- """Build the recommendation request payload."""
56
- payload: dict[str, Any] = {
57
- "userId": user_id,
58
- "topicId": topic_id,
59
- "requestedAt": datetime.now(timezone.utc).isoformat(),
60
- "key": f"{user_id}-{topic_id}",
61
- }
62
- if limit is not None:
63
- payload["limit"] = limit
64
- return payload
65
-
66
- @staticmethod
67
- def _validate(user_id: str, topic_id: str) -> None:
68
- """Validate the recommendation request."""
69
- if not user_id:
70
- raise ValueError("user_id is required")
71
- if not topic_id:
72
- raise ValueError("topic_id is required")