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.
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/PKG-INFO +113 -1
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/README.md +112 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/pyproject.toml +1 -1
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/__init__.py +15 -1
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/client.py +13 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.py +10 -10
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +16 -4
- {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
- {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
- sf_queue_sdk-0.2.0/queue_sdk/query.py +22 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/email_queue.py +43 -5
- sf_queue_sdk-0.2.0/queue_sdk/queues/pdf_to_image_queue.py +92 -0
- sf_queue_sdk-0.2.0/queue_sdk/queues/recommendation_queue.py +136 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/types.py +68 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/.gitignore +0 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/__init__.py +0 -0
- {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
- {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
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/__init__.py +0 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/base_queue.py +0 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/tool_assignment_queue.py +0 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/queues/topic_assignment_queue.py +0 -0
- {sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/redis_client.py +0 -0
- {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.
|
|
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,19 +1,26 @@
|
|
|
1
1
|
"""sf-queue Python SDK - Redis-based queue system."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
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."""
|
{sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.py
RENAMED
|
@@ -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:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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\"\
|
|
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=
|
|
41
|
-
_globals['_BATCHEMAILREQUEST']._serialized_start=
|
|
42
|
-
_globals['_BATCHEMAILREQUEST']._serialized_end=
|
|
43
|
-
_globals['_EMAILRESPONSE']._serialized_start=
|
|
44
|
-
_globals['_EMAILRESPONSE']._serialized_end=
|
|
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)
|
{sf_queue_sdk-0.1.0b16 → sf_queue_sdk-0.2.0}/queue_sdk/generated/queue/email/v1/email_pb2.pyi
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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="",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|