sf-queue-sdk 0.1.0b16__tar.gz → 0.2.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 (24) hide show
  1. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/PKG-INFO +113 -1
  2. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/README.md +112 -0
  3. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/pyproject.toml +1 -1
  4. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/__init__.py +15 -1
  5. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/client.py +13 -0
  6. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.py +10 -10
  7. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +16 -4
  8. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.py +4 -4
  9. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.py +4 -4
  10. sf_queue_sdk-0.2.0/queue_sdk/query.py +22 -0
  11. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/email_queue.py +43 -5
  12. sf_queue_sdk-0.2.0/queue_sdk/queues/pdf_to_image_queue.py +92 -0
  13. sf_queue_sdk-0.2.0/queue_sdk/queues/recommendation_queue.py +136 -0
  14. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/types.py +68 -0
  15. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/.gitignore +0 -0
  16. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/__init__.py +0 -0
  17. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.pyi +0 -0
  18. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.pyi +0 -0
  19. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/__init__.py +0 -0
  20. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/base_queue.py +0 -0
  21. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/tool_assignment_queue.py +0 -0
  22. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/topic_assignment_queue.py +0 -0
  23. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/redis_client.py +0 -0
  24. {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/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.1.0b16
3
+ Version: 0.2.0
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:
@@ -358,6 +385,91 @@ result = client.tool_assignment.send_batch_and_wait(
358
385
  | `current_assignment` | `Optional[str]` | Yes | Current cluster_id (None for new embeddings) |
359
386
  | `new_assignment` | `str` | Yes | The cluster_id to assign to |
360
387
 
388
+ ## Recommendations
389
+
390
+ ### Fire and forget
391
+
392
+ ```python
393
+ result = client.recommendations.create(
394
+ user_id="user_xyz",
395
+ topic_id="topic_abc123",
396
+ limit=10, # optional
397
+ )
398
+ print("Enqueued:", result.message_id)
399
+ ```
400
+
401
+ ### Create and wait for result
402
+
403
+ ```python
404
+ result = client.recommendations.create_and_wait(
405
+ user_id="user_xyz",
406
+ topic_id="topic_abc123",
407
+ limit=10, # optional
408
+ timeout=30,
409
+ )
410
+ print(result.success)
411
+ print(result.message_id)
412
+ ```
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
+
440
+ ### Recommendation Fields
441
+
442
+ | Field | Type | Required | Description |
443
+ |-------|------|----------|-------------|
444
+ | `user_id` | `str` | Yes | User requesting recommendations |
445
+ | `topic_id` | `str` | Yes | Topic context for recommendations |
446
+ | `limit` | `Optional[int]` | No | Max recommendations to return |
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
+
361
473
  ## Cleanup
362
474
 
363
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:
@@ -350,6 +377,91 @@ result = client.tool_assignment.send_batch_and_wait(
350
377
  | `current_assignment` | `Optional[str]` | Yes | Current cluster_id (None for new embeddings) |
351
378
  | `new_assignment` | `str` | Yes | The cluster_id to assign to |
352
379
 
380
+ ## Recommendations
381
+
382
+ ### Fire and forget
383
+
384
+ ```python
385
+ result = client.recommendations.create(
386
+ user_id="user_xyz",
387
+ topic_id="topic_abc123",
388
+ limit=10, # optional
389
+ )
390
+ print("Enqueued:", result.message_id)
391
+ ```
392
+
393
+ ### Create and wait for result
394
+
395
+ ```python
396
+ result = client.recommendations.create_and_wait(
397
+ user_id="user_xyz",
398
+ topic_id="topic_abc123",
399
+ limit=10, # optional
400
+ timeout=30,
401
+ )
402
+ print(result.success)
403
+ print(result.message_id)
404
+ ```
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
+
432
+ ### Recommendation Fields
433
+
434
+ | Field | Type | Required | Description |
435
+ |-------|------|----------|-------------|
436
+ | `user_id` | `str` | Yes | User requesting recommendations |
437
+ | `topic_id` | `str` | Yes | Topic context for recommendations |
438
+ | `limit` | `Optional[int]` | No | Max recommendations to return |
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
+
353
465
  ## Cleanup
354
466
 
355
467
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sf-queue-sdk"
3
- version = "0.1.0b16"
3
+ version = "0.2.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,19 +1,26 @@
1
1
  """sf-queue Python SDK - Redis-based queue system."""
2
2
 
3
- __version__ = "0.1.0b16"
3
+ __version__ = "0.2.0"
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,
14
+ ConvertData,
15
+ ConvertResponse,
11
16
  EmailButton,
12
17
  EmailData,
13
18
  EmailImage,
14
19
  EmailResponse,
15
20
  QueueClientConfig,
16
21
  QueueResponse,
22
+ RecommendationRequestData,
23
+ RecommendationResponse,
17
24
  SendResult,
18
25
  ToolAssignmentData,
19
26
  TopicAssignmentData,
@@ -21,6 +28,7 @@ from queue_sdk.types import (
21
28
 
22
29
  __all__ = [
23
30
  "QueueClient",
31
+ "QueryClient",
24
32
  "QueueClientConfig",
25
33
  "EmailData",
26
34
  "EmailButton",
@@ -29,9 +37,15 @@ __all__ = [
29
37
  "BatchEmailResponse",
30
38
  "QueueResponse",
31
39
  "BatchQueueResponse",
40
+ "RecommendationRequestData",
41
+ "RecommendationResponse",
42
+ "BatchRecommendationRequestData",
43
+ "BatchRecommendationResponse",
32
44
  "TopicAssignmentData",
33
45
  "BatchTopicAssignmentData",
34
46
  "ToolAssignmentData",
35
47
  "BatchToolAssignmentData",
48
+ "ConvertData",
49
+ "ConvertResponse",
36
50
  "SendResult",
37
51
  ]
@@ -1,6 +1,9 @@
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
5
+ from queue_sdk.queues.pdf_to_image_queue import PdfToImageQueue
6
+ from queue_sdk.queues.recommendation_queue import RecommendationQueue
4
7
  from queue_sdk.queues.tool_assignment_queue import ToolAssignmentQueue
5
8
  from queue_sdk.queues.topic_assignment_queue import TopicAssignmentQueue
6
9
  from queue_sdk.redis_client import create_redis_client
@@ -36,6 +39,13 @@ class QueueClient:
36
39
  paragraphs=["Hello!"],
37
40
  timeout=30,
38
41
  )
42
+
43
+ # Convert PDF to image
44
+ result = client.pdf_to_image.convert_and_wait(
45
+ url="https://example.com/document.pdf",
46
+ page=1,
47
+ format="webp",
48
+ )
39
49
  """
40
50
 
41
51
  def __init__(
@@ -56,6 +66,9 @@ class QueueClient:
56
66
  self.email = EmailQueue(self._redis, self._environment)
57
67
  self.topic_assignment = TopicAssignmentQueue(self._redis, self._environment)
58
68
  self.tool_assignment = ToolAssignmentQueue(self._redis, self._environment)
69
+ self.pdf_to_image = PdfToImageQueue(self._redis, self._environment)
70
+ self.recommendations = RecommendationQueue(self._redis, self._environment)
71
+ self.query = QueryClient(self._redis)
59
72
 
60
73
  def disconnect(self) -> None:
61
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\"\xbe\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\x12\x12\n\x04html\x18\n \x01(\tR\x04html\"\xc3\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\x12\x12\n\x04html\x18\n \x01(\tR\x04html\"^\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=516
41
+ _globals['_BATCHEMAILREQUEST']._serialized_start=519
42
+ _globals['_BATCHEMAILREQUEST']._serialized_end=842
43
+ _globals['_EMAILRESPONSE']._serialized_start=844
44
+ _globals['_EMAILRESPONSE']._serialized_end=938
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", "html")
31
31
  TO_FIELD_NUMBER: _ClassVar[int]
32
32
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
33
33
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -35,6 +35,9 @@ 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]
40
+ HTML_FIELD_NUMBER: _ClassVar[int]
38
41
  to: str
39
42
  preview: str
40
43
  subject: str
@@ -42,10 +45,13 @@ class EmailRequest(_message.Message):
42
45
  button: EmailButton
43
46
  reply_to: str
44
47
  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: ...
48
+ service: str
49
+ template: str
50
+ html: str
51
+ 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] = ...) -> None: ...
46
52
 
47
53
  class BatchEmailRequest(_message.Message):
48
- __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image")
54
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image", "service", "template", "html")
49
55
  TO_FIELD_NUMBER: _ClassVar[int]
50
56
  PREVIEW_FIELD_NUMBER: _ClassVar[int]
51
57
  SUBJECT_FIELD_NUMBER: _ClassVar[int]
@@ -53,6 +59,9 @@ class BatchEmailRequest(_message.Message):
53
59
  BUTTON_FIELD_NUMBER: _ClassVar[int]
54
60
  REPLY_TO_FIELD_NUMBER: _ClassVar[int]
55
61
  IMAGE_FIELD_NUMBER: _ClassVar[int]
62
+ SERVICE_FIELD_NUMBER: _ClassVar[int]
63
+ TEMPLATE_FIELD_NUMBER: _ClassVar[int]
64
+ HTML_FIELD_NUMBER: _ClassVar[int]
56
65
  to: _containers.RepeatedScalarFieldContainer[str]
57
66
  preview: str
58
67
  subject: str
@@ -60,7 +69,10 @@ class BatchEmailRequest(_message.Message):
60
69
  button: EmailButton
61
70
  reply_to: str
62
71
  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: ...
72
+ service: str
73
+ template: str
74
+ html: str
75
+ 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] = ...) -> None: ...
64
76
 
65
77
  class EmailResponse(_message.Message):
66
78
  __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,14 @@ 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,
33
+ html: Optional[str] = None,
31
34
  ) -> SendResult:
32
35
  """Enqueue an email for processing (fire and forget)."""
33
- payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
36
+ payload = self._build_payload(
37
+ to, preview, subject, paragraphs, button, reply_to, image, service, template, html
38
+ )
34
39
  message_id = self._enqueue(payload)
35
40
  return SendResult(message_id=message_id)
36
41
 
@@ -44,9 +49,14 @@ class EmailQueue(BaseQueue):
44
49
  reply_to: Optional[str] = None,
45
50
  image: Optional[dict[str, Any]] = None,
46
51
  timeout: Optional[int] = None,
52
+ service: Optional[str] = None,
53
+ template: Optional[str] = None,
54
+ html: Optional[str] = None,
47
55
  ) -> EmailResponse:
48
56
  """Enqueue an email and wait for processing confirmation."""
49
- payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
57
+ payload = self._build_payload(
58
+ to, preview, subject, paragraphs, button, reply_to, image, service, template, html
59
+ )
50
60
  return self._enqueue_and_wait(payload, timeout)
51
61
 
52
62
  def send_batch(
@@ -58,10 +68,15 @@ class EmailQueue(BaseQueue):
58
68
  button: Optional[dict[str, str]] = None,
59
69
  reply_to: Optional[str] = None,
60
70
  image: Optional[dict[str, Any]] = None,
71
+ service: Optional[str] = None,
72
+ template: Optional[str] = None,
73
+ html: Optional[str] = None,
61
74
  ) -> SendResult:
62
75
  """Enqueue a batch email for processing (fire and forget)."""
63
76
  self._validate_batch(to)
64
- payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
77
+ payload = self._build_batch_payload(
78
+ to, preview, subject, paragraphs, button, reply_to, image, service, template, html
79
+ )
65
80
  message_id = self._enqueue(payload)
66
81
  return SendResult(message_id=message_id)
67
82
 
@@ -75,10 +90,15 @@ class EmailQueue(BaseQueue):
75
90
  reply_to: Optional[str] = None,
76
91
  image: Optional[dict[str, Any]] = None,
77
92
  timeout: Optional[int] = None,
93
+ service: Optional[str] = None,
94
+ template: Optional[str] = None,
95
+ html: Optional[str] = None,
78
96
  ) -> BatchEmailResponse:
79
97
  """Enqueue a batch email and wait for processing confirmation."""
80
98
  self._validate_batch(to)
81
- payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
99
+ payload = self._build_batch_payload(
100
+ to, preview, subject, paragraphs, button, reply_to, image, service, template, html
101
+ )
82
102
  response = self._enqueue_and_wait(payload, timeout)
83
103
 
84
104
  return BatchEmailResponse(
@@ -100,6 +120,9 @@ class EmailQueue(BaseQueue):
100
120
  button: Optional[dict[str, str]],
101
121
  reply_to: Optional[str],
102
122
  image: Optional[dict[str, Any]],
123
+ service: Optional[str] = None,
124
+ template: Optional[str] = None,
125
+ html: Optional[str] = None,
103
126
  ) -> dict[str, Any]:
104
127
  """Build and sanitize the email payload."""
105
128
  payload: dict[str, Any] = {
@@ -126,6 +149,15 @@ class EmailQueue(BaseQueue):
126
149
  "height": image.get("height"),
127
150
  }
128
151
 
152
+ if service:
153
+ payload["service"] = service
154
+
155
+ if template:
156
+ payload["template"] = template
157
+
158
+ if html:
159
+ payload["html"] = html
160
+
129
161
  return payload
130
162
 
131
163
  def _build_batch_payload(
@@ -137,16 +169,22 @@ class EmailQueue(BaseQueue):
137
169
  button: Optional[dict[str, str]],
138
170
  reply_to: Optional[str],
139
171
  image: Optional[dict[str, Any]],
172
+ service: Optional[str] = None,
173
+ template: Optional[str] = None,
174
+ html: Optional[str] = None,
140
175
  ) -> dict[str, Any]:
141
176
  """Build and sanitize the batch email payload."""
142
177
  payload = self._build_payload(
143
- to="", # not used for batch
178
+ to="",
144
179
  preview=preview,
145
180
  subject=subject,
146
181
  paragraphs=paragraphs,
147
182
  button=button,
148
183
  reply_to=reply_to,
149
184
  image=image,
185
+ service=service,
186
+ template=template,
187
+ html=html,
150
188
  )
151
189
  payload["to"] = to
152
190
  payload["batch"] = True
@@ -0,0 +1,92 @@
1
+ """PDF to Image queue with typed methods."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import redis as redis_lib
6
+
7
+ from queue_sdk.queues.base_queue import BaseQueue
8
+ from queue_sdk.types import ConvertResponse, SendResult
9
+
10
+
11
+ class PdfToImageQueue(BaseQueue):
12
+ """Typed methods for converting PDFs/videos to images via the queue."""
13
+
14
+ def __init__(self, redis_client: redis_lib.Redis, environment: str):
15
+ super().__init__(redis_client, environment, "pdf-to-image-queue")
16
+
17
+ def convert(
18
+ self,
19
+ url: str,
20
+ page: Optional[int] = None,
21
+ format: Optional[str] = None,
22
+ quality: Optional[int] = None,
23
+ ) -> SendResult:
24
+ """Enqueue a conversion request (fire and forget)."""
25
+ self._validate(url, page, format, quality)
26
+ payload = self._build_payload(url, page, format, quality)
27
+ message_id = self._enqueue(payload)
28
+ return SendResult(message_id=message_id)
29
+
30
+ def convert_and_wait(
31
+ self,
32
+ url: str,
33
+ page: Optional[int] = None,
34
+ format: Optional[str] = None,
35
+ quality: Optional[int] = None,
36
+ timeout: Optional[int] = None,
37
+ ) -> ConvertResponse:
38
+ """Enqueue a conversion request and wait for the result."""
39
+ self._validate(url, page, format, quality)
40
+ payload = self._build_payload(url, page, format, quality)
41
+ response = self._enqueue_and_wait(payload, timeout)
42
+
43
+ return ConvertResponse(
44
+ success=response.success,
45
+ message_id=response.message_id,
46
+ image_url=getattr(response, "imageUrl", None) or getattr(response, "image_url", None),
47
+ format=getattr(response, "format", None),
48
+ filename=getattr(response, "filename", None),
49
+ error=response.error,
50
+ processed_at=response.processed_at,
51
+ )
52
+
53
+ def _build_payload(
54
+ self,
55
+ url: str,
56
+ page: Optional[int],
57
+ format: Optional[str],
58
+ quality: Optional[int],
59
+ ) -> dict[str, Any]:
60
+ """Build the conversion payload."""
61
+ payload: dict[str, Any] = {"url": url}
62
+
63
+ if page is not None:
64
+ payload["page"] = page
65
+
66
+ if format is not None:
67
+ payload["format"] = format
68
+
69
+ if quality is not None:
70
+ payload["quality"] = quality
71
+
72
+ return payload
73
+
74
+ @staticmethod
75
+ def _validate(
76
+ url: str,
77
+ page: Optional[int],
78
+ format: Optional[str],
79
+ quality: Optional[int],
80
+ ) -> None:
81
+ """Validate the conversion request."""
82
+ if not url:
83
+ raise ValueError("url is required")
84
+
85
+ if page is not None and (page < 1 or not isinstance(page, int)):
86
+ raise ValueError("page must be a positive integer")
87
+
88
+ if format is not None and format not in ("png", "webp"):
89
+ raise ValueError("format must be 'png' or 'webp'")
90
+
91
+ if quality is not None and (quality < 1 or quality > 100):
92
+ raise ValueError("quality must be between 1 and 100")
@@ -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,9 @@ 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
47
+ html: Optional[str] = None
45
48
 
46
49
 
47
50
  @dataclass
@@ -55,6 +58,9 @@ class BatchEmailData:
55
58
  button: Optional[EmailButton] = None
56
59
  reply_to: Optional[str] = None
57
60
  image: Optional[EmailImage] = None
61
+ service: Optional[str] = None
62
+ template: Optional[str] = None
63
+ html: Optional[str] = None
58
64
 
59
65
 
60
66
  @dataclass
@@ -126,3 +132,65 @@ class BatchToolAssignmentData:
126
132
  """Data for multiple tool assignment changes."""
127
133
 
128
134
  assignments: list[ToolAssignmentData]
135
+
136
+
137
+ @dataclass
138
+ class RecommendationRequestData:
139
+ """Data for a recommendation request (mirrors Kafka recommendationRequests)."""
140
+
141
+ user_id: str
142
+ topic_id: str
143
+ limit: Optional[int] = None
144
+
145
+
146
+ @dataclass
147
+ class RecommendationResponse:
148
+ """Response from a processed recommendation request."""
149
+
150
+ success: bool
151
+ message_id: str
152
+ error: Optional[str] = None
153
+ processed_at: Optional[str] = None
154
+
155
+
156
+ @dataclass
157
+ class BatchRecommendationRequestData:
158
+ """Data for multiple recommendation requests."""
159
+
160
+ requests: list[dict]
161
+
162
+
163
+ @dataclass
164
+ class BatchRecommendationResponse:
165
+ """Response from a processed batch of recommendation requests."""
166
+
167
+ success: bool
168
+ message_id: str
169
+ total: int
170
+ successful: int
171
+ failed: int
172
+ error: Optional[str] = None
173
+ processed_at: Optional[str] = None
174
+
175
+
176
+ @dataclass
177
+ class ConvertData:
178
+ """Data for a PDF/video to image conversion request."""
179
+
180
+ url: str
181
+ page: Optional[int] = None
182
+ format: Optional[str] = None
183
+ quality: Optional[int] = None
184
+
185
+
186
+ @dataclass
187
+ class ConvertResponse:
188
+ """Response from a processed conversion request."""
189
+
190
+ success: bool
191
+ message_id: str
192
+ image_url: Optional[str] = None
193
+ format: Optional[str] = None
194
+ filename: Optional[str] = None
195
+ error: Optional[str] = None
196
+ processed_at: Optional[str] = None
File without changes