sf-queue-sdk 0.5.0b4__tar.gz → 0.5.0b30__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.5.0b4 → sf_queue_sdk-0.5.0b30}/.gitignore +1 -0
- sf_queue_sdk-0.5.0b4/README.md → sf_queue_sdk-0.5.0b30/PKG-INFO +36 -0
- sf_queue_sdk-0.5.0b4/PKG-INFO → sf_queue_sdk-0.5.0b30/README.md +28 -8
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/pyproject.toml +1 -1
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/__init__.py +1 -1
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/email/v1/email_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/base_queue.py +52 -1
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/document_upload_queue.py +9 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/redis_client.py +10 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/types.py +1 -0
- sf_queue_sdk-0.5.0b30/tests/test_idempotency.py +109 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/client.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/__init__.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/query.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/__init__.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/document_notes_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/document_persist_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/email_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/pdf_to_image_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/prompt_ingest_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/recommendation_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/tool_assignment_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/queues/topic_assignment_queue.py +0 -0
- {sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/sanitize.py +0 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sf-queue-sdk
|
|
3
|
+
Version: 0.5.0b30
|
|
4
|
+
Summary: Python SDK for sf-queue - Redis-based queue system
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: redis>=5.0.0
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
1
9
|
# sf-queue-sdk
|
|
2
10
|
|
|
3
11
|
Python SDK for sf-queue. Enqueues emails via Redis Streams with optional blocking confirmation from the Go consumer service.
|
|
@@ -546,6 +554,33 @@ result = client.document_upload.send(
|
|
|
546
554
|
print("Enqueued:", result.message_id)
|
|
547
555
|
```
|
|
548
556
|
|
|
557
|
+
### Idempotent verified uploads
|
|
558
|
+
|
|
559
|
+
Pass `idempotency_key` when retrying a verified upload. Calls with the same
|
|
560
|
+
environment, queue, and key create one Redis stream entry and return the same
|
|
561
|
+
`message_id`.
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
result = client.document_upload.send(
|
|
565
|
+
uploaded_file_id="file_abc123",
|
|
566
|
+
study_set_id="set_xyz",
|
|
567
|
+
user_id="user_123",
|
|
568
|
+
text_extract_s3_key="uploadedfiles/file_abc123/textContent",
|
|
569
|
+
pdf_s3_key="uploads/document.pdf",
|
|
570
|
+
file_type="application/pdf",
|
|
571
|
+
effective_file_type="application/pdf",
|
|
572
|
+
filename="Chapter 5 - Thermodynamics.pdf",
|
|
573
|
+
material_title="Thermodynamics",
|
|
574
|
+
url="https://storage.example.com/uploads/document.pdf",
|
|
575
|
+
generate_notes=True,
|
|
576
|
+
idempotency_key="verified-upload:file_abc123",
|
|
577
|
+
)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
The key is scoped to `document-upload` and the configured environment. The SDK
|
|
581
|
+
stores a SHA-256 digest of the caller key in Redis and keeps the idempotency
|
|
582
|
+
mapping for seven days.
|
|
583
|
+
|
|
549
584
|
### With optional fields
|
|
550
585
|
|
|
551
586
|
```python
|
|
@@ -606,6 +641,7 @@ result = client.document_upload.send(
|
|
|
606
641
|
| `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
|
|
607
642
|
| `existing_material_id` | `Optional[str]` | No | ID of an existing material to link to topics instead of creating new ones (used with `skip_material_creation`) |
|
|
608
643
|
| `params` | `Optional[dict]` | No | Typed extensible flags for pipeline behavior (see below) |
|
|
644
|
+
| `idempotency_key` | `Optional[str]` | No | Caller key for retry-safe enqueueing. Duplicate environment/queue/key sends return the original `message_id`. |
|
|
609
645
|
|
|
610
646
|
#### `params` fields
|
|
611
647
|
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: sf-queue-sdk
|
|
3
|
-
Version: 0.5.0b4
|
|
4
|
-
Summary: Python SDK for sf-queue - Redis-based queue system
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Requires-Dist: redis>=5.0.0
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
|
|
9
1
|
# sf-queue-sdk
|
|
10
2
|
|
|
11
3
|
Python SDK for sf-queue. Enqueues emails via Redis Streams with optional blocking confirmation from the Go consumer service.
|
|
@@ -554,6 +546,33 @@ result = client.document_upload.send(
|
|
|
554
546
|
print("Enqueued:", result.message_id)
|
|
555
547
|
```
|
|
556
548
|
|
|
549
|
+
### Idempotent verified uploads
|
|
550
|
+
|
|
551
|
+
Pass `idempotency_key` when retrying a verified upload. Calls with the same
|
|
552
|
+
environment, queue, and key create one Redis stream entry and return the same
|
|
553
|
+
`message_id`.
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
result = client.document_upload.send(
|
|
557
|
+
uploaded_file_id="file_abc123",
|
|
558
|
+
study_set_id="set_xyz",
|
|
559
|
+
user_id="user_123",
|
|
560
|
+
text_extract_s3_key="uploadedfiles/file_abc123/textContent",
|
|
561
|
+
pdf_s3_key="uploads/document.pdf",
|
|
562
|
+
file_type="application/pdf",
|
|
563
|
+
effective_file_type="application/pdf",
|
|
564
|
+
filename="Chapter 5 - Thermodynamics.pdf",
|
|
565
|
+
material_title="Thermodynamics",
|
|
566
|
+
url="https://storage.example.com/uploads/document.pdf",
|
|
567
|
+
generate_notes=True,
|
|
568
|
+
idempotency_key="verified-upload:file_abc123",
|
|
569
|
+
)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
The key is scoped to `document-upload` and the configured environment. The SDK
|
|
573
|
+
stores a SHA-256 digest of the caller key in Redis and keeps the idempotency
|
|
574
|
+
mapping for seven days.
|
|
575
|
+
|
|
557
576
|
### With optional fields
|
|
558
577
|
|
|
559
578
|
```python
|
|
@@ -614,6 +633,7 @@ result = client.document_upload.send(
|
|
|
614
633
|
| `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
|
|
615
634
|
| `existing_material_id` | `Optional[str]` | No | ID of an existing material to link to topics instead of creating new ones (used with `skip_material_creation`) |
|
|
616
635
|
| `params` | `Optional[dict]` | No | Typed extensible flags for pipeline behavior (see below) |
|
|
636
|
+
| `idempotency_key` | `Optional[str]` | No | Caller key for retry-safe enqueueing. Duplicate environment/queue/key sends return the original `message_id`. |
|
|
617
637
|
|
|
618
638
|
#### `params` fields
|
|
619
639
|
|
{sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/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: 7.
|
|
5
|
+
# Protobuf Python Version: 7.36.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
|
|
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|
|
12
12
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
13
|
_runtime_version.Domain.PUBLIC,
|
|
14
14
|
7,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
36,
|
|
16
|
+
0,
|
|
17
17
|
'',
|
|
18
18
|
'queue/email/v1/email.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/prompt_ingest/v1/prompt_ingest.proto
|
|
5
|
-
# Protobuf Python Version: 7.
|
|
5
|
+
# Protobuf Python Version: 7.36.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
|
|
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|
|
12
12
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
13
|
_runtime_version.Domain.PUBLIC,
|
|
14
14
|
7,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
36,
|
|
16
|
+
0,
|
|
17
17
|
'',
|
|
18
18
|
'queue/prompt_ingest/v1/prompt_ingest.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/tool_assignment/v1/tool_assignment.proto
|
|
5
|
-
# Protobuf Python Version: 7.
|
|
5
|
+
# Protobuf Python Version: 7.36.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
|
|
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|
|
12
12
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
13
|
_runtime_version.Domain.PUBLIC,
|
|
14
14
|
7,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
36,
|
|
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: 7.
|
|
5
|
+
# Protobuf Python Version: 7.36.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
|
|
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
|
|
|
12
12
|
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
13
|
_runtime_version.Domain.PUBLIC,
|
|
14
14
|
7,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
36,
|
|
16
|
+
0,
|
|
17
17
|
'',
|
|
18
18
|
'queue/topic_assignment/v1/topic_assignment.proto'
|
|
19
19
|
)
|
|
@@ -7,10 +7,32 @@ from typing import Any, Optional
|
|
|
7
7
|
|
|
8
8
|
import redis as redis_lib
|
|
9
9
|
|
|
10
|
-
from queue_sdk.redis_client import response_key, stream_name
|
|
10
|
+
from queue_sdk.redis_client import idempotency_key, response_key, stream_name
|
|
11
11
|
from queue_sdk.types import EmailResponse
|
|
12
12
|
|
|
13
13
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
14
|
+
IDEMPOTENCY_TTL_SECONDS = 7 * 24 * 60 * 60
|
|
15
|
+
IDEMPOTENT_ENQUEUE_SCRIPT = """
|
|
16
|
+
local existing_request_id = redis.call("GET", KEYS[2])
|
|
17
|
+
if existing_request_id then
|
|
18
|
+
return {existing_request_id, 0}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
redis.call(
|
|
22
|
+
"XADD",
|
|
23
|
+
KEYS[1],
|
|
24
|
+
"*",
|
|
25
|
+
"requestId",
|
|
26
|
+
ARGV[1],
|
|
27
|
+
"payload",
|
|
28
|
+
ARGV[2],
|
|
29
|
+
"createdAt",
|
|
30
|
+
ARGV[3]
|
|
31
|
+
)
|
|
32
|
+
redis.call("SET", KEYS[2], ARGV[1], "EX", ARGV[4])
|
|
33
|
+
|
|
34
|
+
return {ARGV[1], 1}
|
|
35
|
+
"""
|
|
14
36
|
|
|
15
37
|
|
|
16
38
|
class BaseQueue:
|
|
@@ -35,6 +57,35 @@ class BaseQueue:
|
|
|
35
57
|
self._redis.xadd(self._stream, message)
|
|
36
58
|
return request_id
|
|
37
59
|
|
|
60
|
+
def _enqueue_idempotently(
|
|
61
|
+
self, payload: dict[str, Any], caller_key: str
|
|
62
|
+
) -> tuple[str, bool]:
|
|
63
|
+
"""Add one stream entry for a caller key."""
|
|
64
|
+
if not caller_key.strip():
|
|
65
|
+
raise ValueError("idempotency_key must not be empty")
|
|
66
|
+
|
|
67
|
+
request_id = str(uuid.uuid4())
|
|
68
|
+
created_at = datetime.now(timezone.utc).isoformat()
|
|
69
|
+
result = self._redis.eval(
|
|
70
|
+
IDEMPOTENT_ENQUEUE_SCRIPT,
|
|
71
|
+
2,
|
|
72
|
+
self._stream,
|
|
73
|
+
idempotency_key(self._environment, self._queue_name, caller_key),
|
|
74
|
+
request_id,
|
|
75
|
+
json.dumps(payload),
|
|
76
|
+
created_at,
|
|
77
|
+
IDEMPOTENCY_TTL_SECONDS,
|
|
78
|
+
)
|
|
79
|
+
if (
|
|
80
|
+
not isinstance(result, list)
|
|
81
|
+
or len(result) != 2
|
|
82
|
+
or not isinstance(result[0], str)
|
|
83
|
+
or result[1] not in (0, 1)
|
|
84
|
+
):
|
|
85
|
+
raise RuntimeError("Redis returned an invalid idempotent enqueue result")
|
|
86
|
+
|
|
87
|
+
return result[0], result[1] == 1
|
|
88
|
+
|
|
38
89
|
def _enqueue_and_wait(
|
|
39
90
|
self, payload: dict[str, Any], timeout: Optional[int] = None
|
|
40
91
|
) -> EmailResponse:
|
|
@@ -53,6 +53,7 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
53
53
|
skip_material_creation: Optional[bool] = None,
|
|
54
54
|
existing_material_id: Optional[str] = None,
|
|
55
55
|
params: Optional[dict] = None,
|
|
56
|
+
idempotency_key: Optional[str] = None,
|
|
56
57
|
) -> SendResult:
|
|
57
58
|
"""Enqueue a document upload processing request (fire and forget)."""
|
|
58
59
|
self._validate(uploaded_file_id, study_set_id, user_id, pdf_s3_key, filename)
|
|
@@ -91,8 +92,16 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
91
92
|
existing_material_id=existing_material_id,
|
|
92
93
|
params=params,
|
|
93
94
|
)
|
|
95
|
+
if idempotency_key is not None:
|
|
96
|
+
message_id, enqueued = self._enqueue_idempotently(payload, idempotency_key)
|
|
97
|
+
if enqueued:
|
|
98
|
+
self._set_initial_status(message_id)
|
|
99
|
+
|
|
100
|
+
return SendResult(message_id=message_id)
|
|
101
|
+
|
|
94
102
|
message_id = self._enqueue(payload)
|
|
95
103
|
self._set_initial_status(message_id)
|
|
104
|
+
|
|
96
105
|
return SendResult(message_id=message_id)
|
|
97
106
|
|
|
98
107
|
def get_status(self, request_id: str) -> DocumentUploadStatus:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Redis connection factory."""
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
from urllib.parse import urlparse
|
|
4
5
|
|
|
5
6
|
import redis
|
|
@@ -45,6 +46,15 @@ def stream_name(env: str, base: str) -> str:
|
|
|
45
46
|
return f"{env}:{base}"
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def idempotency_key(env: str, queue: str, caller_key: str) -> str:
|
|
50
|
+
"""Return a bounded key in the queue's Redis Cluster hash slot."""
|
|
51
|
+
digest = hashlib.sha256(caller_key.encode("utf-8")).hexdigest()
|
|
52
|
+
key = f"{{{queue}}}:idempotency:{digest}"
|
|
53
|
+
if not env:
|
|
54
|
+
return key
|
|
55
|
+
return f"{env}:{key}"
|
|
56
|
+
|
|
57
|
+
|
|
48
58
|
def response_key(env: str, queue: str, request_id: str) -> str:
|
|
49
59
|
"""Return the response list key for a given queue and request ID."""
|
|
50
60
|
key = f"{queue}:response:{request_id}"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Focused idempotent enqueue tests."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from queue_sdk.queues.document_upload_queue import DocumentUploadQueue
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeRedis:
|
|
9
|
+
"""Redis test double for the idempotent enqueue script."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.idempotency: dict[str, str] = {}
|
|
13
|
+
self.entries: list[dict[str, str]] = []
|
|
14
|
+
self.status_writes = 0
|
|
15
|
+
|
|
16
|
+
def eval(
|
|
17
|
+
self,
|
|
18
|
+
_script: str,
|
|
19
|
+
key_count: int,
|
|
20
|
+
stream: str,
|
|
21
|
+
deduplication_key: str,
|
|
22
|
+
request_id: str,
|
|
23
|
+
payload: str,
|
|
24
|
+
created_at: str,
|
|
25
|
+
ttl_seconds: int,
|
|
26
|
+
) -> list[str | int]:
|
|
27
|
+
if key_count != 2:
|
|
28
|
+
raise AssertionError("expected two Redis keys")
|
|
29
|
+
if "{document-upload}" not in stream:
|
|
30
|
+
raise AssertionError("stream is missing the queue hash tag")
|
|
31
|
+
if "{document-upload}:idempotency:" not in deduplication_key:
|
|
32
|
+
raise AssertionError("idempotency key is in a different hash slot")
|
|
33
|
+
if ttl_seconds != 7 * 24 * 60 * 60:
|
|
34
|
+
raise AssertionError("unexpected idempotency key TTL")
|
|
35
|
+
|
|
36
|
+
existing_request_id = self.idempotency.get(deduplication_key)
|
|
37
|
+
if existing_request_id is not None:
|
|
38
|
+
return [existing_request_id, 0]
|
|
39
|
+
|
|
40
|
+
self.entries.append(
|
|
41
|
+
{
|
|
42
|
+
"stream": stream,
|
|
43
|
+
"request_id": request_id,
|
|
44
|
+
"payload": payload,
|
|
45
|
+
"created_at": created_at,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
self.idempotency[deduplication_key] = request_id
|
|
49
|
+
|
|
50
|
+
return [request_id, 1]
|
|
51
|
+
|
|
52
|
+
def hset(self, *_args: object, **_kwargs: object) -> None:
|
|
53
|
+
self.status_writes += 1
|
|
54
|
+
|
|
55
|
+
def expire(self, *_args: object, **_kwargs: object) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def send_upload(
|
|
60
|
+
queue: DocumentUploadQueue, idempotency_key: str
|
|
61
|
+
):
|
|
62
|
+
"""Send the minimum valid document upload payload."""
|
|
63
|
+
return queue.send(
|
|
64
|
+
uploaded_file_id="file-1",
|
|
65
|
+
study_set_id="set-1",
|
|
66
|
+
user_id="user-1",
|
|
67
|
+
text_extract_s3_key="text/key",
|
|
68
|
+
pdf_s3_key="pdf/key",
|
|
69
|
+
file_type="application/pdf",
|
|
70
|
+
effective_file_type="application/pdf",
|
|
71
|
+
filename="notes.pdf",
|
|
72
|
+
material_title="Notes",
|
|
73
|
+
url="https://example.com/notes.pdf",
|
|
74
|
+
generate_notes=True,
|
|
75
|
+
idempotency_key=idempotency_key,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class IdempotentDocumentUploadTests(unittest.TestCase):
|
|
80
|
+
"""Document upload idempotency contract."""
|
|
81
|
+
|
|
82
|
+
def setUp(self) -> None:
|
|
83
|
+
self.redis = FakeRedis()
|
|
84
|
+
self.queue = DocumentUploadQueue(self.redis, "staging")
|
|
85
|
+
|
|
86
|
+
def test_duplicate_key_enqueues_once_and_returns_one_request_id(self) -> None:
|
|
87
|
+
first = send_upload(self.queue, "verified-upload-1")
|
|
88
|
+
duplicate = send_upload(self.queue, "verified-upload-1")
|
|
89
|
+
|
|
90
|
+
self.assertEqual(duplicate.message_id, first.message_id)
|
|
91
|
+
self.assertEqual(len(self.redis.entries), 1)
|
|
92
|
+
self.assertEqual(self.redis.status_writes, 1)
|
|
93
|
+
|
|
94
|
+
def test_different_keys_create_different_stream_entries(self) -> None:
|
|
95
|
+
first = send_upload(self.queue, "verified-upload-1")
|
|
96
|
+
second = send_upload(self.queue, "verified-upload-2")
|
|
97
|
+
|
|
98
|
+
self.assertNotEqual(second.message_id, first.message_id)
|
|
99
|
+
self.assertEqual(len(self.redis.entries), 2)
|
|
100
|
+
|
|
101
|
+
def test_empty_key_is_rejected(self) -> None:
|
|
102
|
+
with self.assertRaisesRegex(ValueError, "idempotency_key must not be empty"):
|
|
103
|
+
send_upload(self.queue, " ")
|
|
104
|
+
|
|
105
|
+
self.assertEqual(len(self.redis.entries), 0)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
{sf_queue_sdk-0.5.0b4 → sf_queue_sdk-0.5.0b30}/queue_sdk/generated/queue/email/v1/email_pb2.pyi
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|