sf-queue-sdk 0.5.0b56__tar.gz → 0.5.0b79__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.0b56 → sf_queue_sdk-0.5.0b79}/.gitignore +2 -0
- sf_queue_sdk-0.5.0b56/README.md → sf_queue_sdk-0.5.0b79/PKG-INFO +20 -1
- sf_queue_sdk-0.5.0b56/PKG-INFO → sf_queue_sdk-0.5.0b79/README.md +12 -9
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/pyproject.toml +1 -1
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/__init__.py +1 -1
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/email/v1/email_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.py +3 -3
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/document_upload_queue.py +33 -5
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/redis_client.py +10 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/types.py +1 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/tests/test_idempotency.py +78 -5
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/client.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/__init__.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/email/v1/email_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/prompt_ingest/v1/prompt_ingest_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/tool_assignment/v1/tool_assignment_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/generated/queue/topic_assignment/v1/topic_assignment_pb2.pyi +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/query.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/__init__.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/base_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/document_notes_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/document_persist_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/email_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/pdf_to_image_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/prompt_ingest_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/recommendation_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/tool_assignment_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/queues/topic_assignment_queue.py +0 -0
- {sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/queue_sdk/sanitize.py +0 -0
|
@@ -57,6 +57,7 @@ coverage/
|
|
|
57
57
|
TODO-document-upload-queue.md
|
|
58
58
|
|
|
59
59
|
# Local pipeline test output
|
|
60
|
+
benchmark-results/
|
|
60
61
|
scripts/.env
|
|
61
62
|
scripts/local-pdfs/
|
|
62
63
|
scripts/mp3/
|
|
@@ -66,3 +67,4 @@ scripts/mp3-results/
|
|
|
66
67
|
scripts/studyset/.env
|
|
67
68
|
scripts/studyset/downloads/
|
|
68
69
|
scripts/failed-files/
|
|
70
|
+
benchmark-results/
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sf-queue-sdk
|
|
3
|
+
Version: 0.5.0b79
|
|
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.
|
|
@@ -598,9 +606,19 @@ result = client.document_upload.send(
|
|
|
598
606
|
material_group_id="group_123",
|
|
599
607
|
folder_id="folder_456",
|
|
600
608
|
upload_batch_id="batch_789",
|
|
609
|
+
content_hash="sha256:verified-content-hash",
|
|
601
610
|
)
|
|
602
611
|
```
|
|
603
612
|
|
|
613
|
+
### Cancel a document
|
|
614
|
+
|
|
615
|
+
Record a manual cancellation before or during processing. Cancellation is
|
|
616
|
+
scoped to the configured environment and retained for seven days.
|
|
617
|
+
|
|
618
|
+
```python
|
|
619
|
+
client.document_upload.cancel_document("file_abc123")
|
|
620
|
+
```
|
|
621
|
+
|
|
604
622
|
### Document Upload Fields
|
|
605
623
|
|
|
606
624
|
| Field | Type | Required | Description |
|
|
@@ -629,6 +647,7 @@ result = client.document_upload.send(
|
|
|
629
647
|
| `material_group_id` | `Optional[str]` | No | Material group for organization |
|
|
630
648
|
| `folder_id` | `Optional[str]` | No | Folder for organization |
|
|
631
649
|
| `upload_batch_id` | `Optional[str]` | No | Batch ID when multiple files uploaded together |
|
|
650
|
+
| `content_hash` | `Optional[str]` | No | Stable source-content identity used to supersede older duplicate batches |
|
|
632
651
|
| `youtube_title` | `Optional[str]` | No | Title override for YouTube video transcripts |
|
|
633
652
|
| `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
|
|
634
653
|
| `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`) |
|
|
@@ -670,7 +689,7 @@ result = client.document_upload.send(
|
|
|
670
689
|
|
|
671
690
|
# Poll for progress
|
|
672
691
|
status = client.document_upload.get_status(result.message_id)
|
|
673
|
-
print(f"Status: {status.status}") #
|
|
692
|
+
print(f"Status: {status.status}") # Includes "cancelled" and "superseded" terminal states
|
|
674
693
|
print(f"Step: {status.step} ({status.step_number}/{status.total_steps})")
|
|
675
694
|
print(f"Progress: {status.percent_complete}%")
|
|
676
695
|
print(f"Detail: {status.detail}") # e.g. "Generated notes for topic 3/7"
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: sf-queue-sdk
|
|
3
|
-
Version: 0.5.0b56
|
|
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.
|
|
@@ -606,9 +598,19 @@ result = client.document_upload.send(
|
|
|
606
598
|
material_group_id="group_123",
|
|
607
599
|
folder_id="folder_456",
|
|
608
600
|
upload_batch_id="batch_789",
|
|
601
|
+
content_hash="sha256:verified-content-hash",
|
|
609
602
|
)
|
|
610
603
|
```
|
|
611
604
|
|
|
605
|
+
### Cancel a document
|
|
606
|
+
|
|
607
|
+
Record a manual cancellation before or during processing. Cancellation is
|
|
608
|
+
scoped to the configured environment and retained for seven days.
|
|
609
|
+
|
|
610
|
+
```python
|
|
611
|
+
client.document_upload.cancel_document("file_abc123")
|
|
612
|
+
```
|
|
613
|
+
|
|
612
614
|
### Document Upload Fields
|
|
613
615
|
|
|
614
616
|
| Field | Type | Required | Description |
|
|
@@ -637,6 +639,7 @@ result = client.document_upload.send(
|
|
|
637
639
|
| `material_group_id` | `Optional[str]` | No | Material group for organization |
|
|
638
640
|
| `folder_id` | `Optional[str]` | No | Folder for organization |
|
|
639
641
|
| `upload_batch_id` | `Optional[str]` | No | Batch ID when multiple files uploaded together |
|
|
642
|
+
| `content_hash` | `Optional[str]` | No | Stable source-content identity used to supersede older duplicate batches |
|
|
640
643
|
| `youtube_title` | `Optional[str]` | No | Title override for YouTube video transcripts |
|
|
641
644
|
| `skip_material_creation` | `Optional[bool]` | No | Skip folder and material creation when the material already exists (e.g. live lectures) |
|
|
642
645
|
| `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`) |
|
|
@@ -678,7 +681,7 @@ result = client.document_upload.send(
|
|
|
678
681
|
|
|
679
682
|
# Poll for progress
|
|
680
683
|
status = client.document_upload.get_status(result.message_id)
|
|
681
|
-
print(f"Status: {status.status}") #
|
|
684
|
+
print(f"Status: {status.status}") # Includes "cancelled" and "superseded" terminal states
|
|
682
685
|
print(f"Step: {status.step} ({status.step_number}/{status.total_steps})")
|
|
683
686
|
print(f"Progress: {status.percent_complete}%")
|
|
684
687
|
print(f"Detail: {status.detail}") # e.g. "Generated notes for topic 3/7"
|
{sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/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.35.1
|
|
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
|
+
35,
|
|
16
|
+
1,
|
|
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.35.1
|
|
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
|
+
35,
|
|
16
|
+
1,
|
|
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.35.1
|
|
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
|
+
35,
|
|
16
|
+
1,
|
|
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.35.1
|
|
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
|
+
35,
|
|
16
|
+
1,
|
|
17
17
|
'',
|
|
18
18
|
'queue/topic_assignment/v1/topic_assignment.proto'
|
|
19
19
|
)
|
|
@@ -6,10 +6,11 @@ from typing import Any, Optional
|
|
|
6
6
|
import redis as redis_lib
|
|
7
7
|
|
|
8
8
|
from queue_sdk.queues.base_queue import BaseQueue
|
|
9
|
-
from queue_sdk.redis_client import status_key
|
|
9
|
+
from queue_sdk.redis_client import document_control_key, status_key
|
|
10
10
|
from queue_sdk.types import DocumentUploadStatus, SendResult
|
|
11
11
|
|
|
12
|
-
STATUS_TTL_SECONDS =
|
|
12
|
+
STATUS_TTL_SECONDS = 7200
|
|
13
|
+
TERMINAL_CONTROL_TTL_SECONDS = 7 * 24 * 60 * 60
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class DocumentUploadQueue(BaseQueue):
|
|
@@ -45,6 +46,7 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
45
46
|
folder_id: Optional[str] = None,
|
|
46
47
|
upload_batch_id: Optional[str] = None,
|
|
47
48
|
batch_size: Optional[int] = None,
|
|
49
|
+
content_hash: Optional[str] = None,
|
|
48
50
|
youtube_title: Optional[str] = None,
|
|
49
51
|
extracted_content: Optional[str] = None,
|
|
50
52
|
callback_url: Optional[str] = None,
|
|
@@ -83,6 +85,7 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
83
85
|
folder_id=folder_id,
|
|
84
86
|
upload_batch_id=upload_batch_id,
|
|
85
87
|
batch_size=batch_size,
|
|
88
|
+
content_hash=content_hash,
|
|
86
89
|
youtube_title=youtube_title,
|
|
87
90
|
extracted_content=extracted_content,
|
|
88
91
|
callback_url=callback_url,
|
|
@@ -133,11 +136,29 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
133
136
|
error=data.get("error", ""),
|
|
134
137
|
)
|
|
135
138
|
|
|
139
|
+
def cancel_document(self, uploaded_file_id: str) -> None:
|
|
140
|
+
"""Record a manual cancellation before or during processing."""
|
|
141
|
+
if not uploaded_file_id.strip():
|
|
142
|
+
raise ValueError("uploaded_file_id must not be empty")
|
|
143
|
+
|
|
144
|
+
key = document_control_key(self._environment, uploaded_file_id)
|
|
145
|
+
pipe = self._redis.pipeline(transaction=False)
|
|
146
|
+
pipe.hset(
|
|
147
|
+
key,
|
|
148
|
+
mapping={
|
|
149
|
+
"manualCancel": "1",
|
|
150
|
+
"cancelReason": "manual",
|
|
151
|
+
"cancelledAt": datetime.now(timezone.utc).isoformat(),
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
pipe.expire(key, TERMINAL_CONTROL_TTL_SECONDS)
|
|
155
|
+
pipe.execute()
|
|
156
|
+
|
|
136
157
|
def _set_initial_status(self, request_id: str) -> None:
|
|
137
158
|
"""Write the initial queued status hash after enqueuing."""
|
|
138
159
|
key = status_key(self._environment, self._queue_name, request_id)
|
|
139
160
|
now = datetime.now(timezone.utc).isoformat()
|
|
140
|
-
|
|
161
|
+
initial_status = {
|
|
141
162
|
"status": "queued",
|
|
142
163
|
"step": "",
|
|
143
164
|
"stepNumber": "0",
|
|
@@ -148,8 +169,12 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
148
169
|
"startedAt": "",
|
|
149
170
|
"updatedAt": now,
|
|
150
171
|
"error": "",
|
|
151
|
-
}
|
|
152
|
-
self._redis.
|
|
172
|
+
}
|
|
173
|
+
pipe = self._redis.pipeline(transaction=False)
|
|
174
|
+
for field, value in initial_status.items():
|
|
175
|
+
pipe.hsetnx(key, field, value)
|
|
176
|
+
pipe.expire(key, STATUS_TTL_SECONDS)
|
|
177
|
+
pipe.execute()
|
|
153
178
|
|
|
154
179
|
@staticmethod
|
|
155
180
|
def _build_payload(
|
|
@@ -178,6 +203,7 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
178
203
|
folder_id: Optional[str],
|
|
179
204
|
upload_batch_id: Optional[str],
|
|
180
205
|
batch_size: Optional[int],
|
|
206
|
+
content_hash: Optional[str],
|
|
181
207
|
youtube_title: Optional[str],
|
|
182
208
|
extracted_content: Optional[str],
|
|
183
209
|
callback_url: Optional[str],
|
|
@@ -225,6 +251,8 @@ class DocumentUploadQueue(BaseQueue):
|
|
|
225
251
|
payload["uploadBatchId"] = upload_batch_id
|
|
226
252
|
if batch_size is not None:
|
|
227
253
|
payload["batchSize"] = batch_size
|
|
254
|
+
if content_hash is not None:
|
|
255
|
+
payload["contentHash"] = content_hash
|
|
228
256
|
if youtube_title is not None:
|
|
229
257
|
payload["youtubeTitle"] = youtube_title
|
|
230
258
|
if extracted_content is not None:
|
|
@@ -55,6 +55,16 @@ def idempotency_key(env: str, queue: str, caller_key: str) -> str:
|
|
|
55
55
|
return f"{env}:{key}"
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def document_control_key(env: str, uploaded_file_id: str) -> str:
|
|
59
|
+
"""Return the cancellation-control key for an upload."""
|
|
60
|
+
digest = hashlib.sha256(uploaded_file_id.encode("utf-8")).hexdigest()
|
|
61
|
+
key = f"{{document-upload}}:ctl:document:{digest}"
|
|
62
|
+
if not env:
|
|
63
|
+
return key
|
|
64
|
+
|
|
65
|
+
return f"{env}:{key}"
|
|
66
|
+
|
|
67
|
+
|
|
58
68
|
def response_key(env: str, queue: str, request_id: str) -> str:
|
|
59
69
|
"""Return the response list key for a given queue and request ID."""
|
|
60
70
|
key = f"{queue}:response:{request_id}"
|
|
@@ -209,6 +209,7 @@ class DocumentUploadData:
|
|
|
209
209
|
material_group_id: Optional[str] = None
|
|
210
210
|
folder_id: Optional[str] = None
|
|
211
211
|
upload_batch_id: Optional[str] = None
|
|
212
|
+
content_hash: Optional[str] = None
|
|
212
213
|
youtube_title: Optional[str] = None
|
|
213
214
|
extracted_content: Optional[str] = None
|
|
214
215
|
callback_url: Optional[str] = None
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
"""Focused idempotent enqueue tests."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import unittest
|
|
4
5
|
|
|
5
6
|
from queue_sdk.queues.document_upload_queue import DocumentUploadQueue
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
class FakePipeline:
|
|
10
|
+
def __init__(self, redis: "FakeRedis") -> None:
|
|
11
|
+
self.redis = redis
|
|
12
|
+
self.operations: list[tuple[str, tuple[object, ...]]] = []
|
|
13
|
+
|
|
14
|
+
def hsetnx(self, *args: object) -> "FakePipeline":
|
|
15
|
+
self.operations.append(("hsetnx", args))
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def hset(self, *args: object, **kwargs: object) -> "FakePipeline":
|
|
19
|
+
self.operations.append(("hset", (*args, kwargs)))
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def expire(self, *args: object) -> "FakePipeline":
|
|
23
|
+
self.operations.append(("expire", args))
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def execute(self) -> None:
|
|
27
|
+
hsetnx_count = sum(
|
|
28
|
+
operation == "hsetnx"
|
|
29
|
+
for operation, _ in self.operations
|
|
30
|
+
)
|
|
31
|
+
if hsetnx_count:
|
|
32
|
+
if hsetnx_count != 10:
|
|
33
|
+
raise AssertionError("unexpected initial status field count")
|
|
34
|
+
if self.operations[-1][1][1] != 7200:
|
|
35
|
+
raise AssertionError("unexpected status TTL")
|
|
36
|
+
self.redis.status_writes += 1
|
|
37
|
+
else:
|
|
38
|
+
self.redis.cancel_writes.append(self.operations)
|
|
39
|
+
|
|
40
|
+
|
|
8
41
|
class FakeRedis:
|
|
9
42
|
"""Redis test double for the idempotent enqueue script."""
|
|
10
43
|
|
|
@@ -12,6 +45,7 @@ class FakeRedis:
|
|
|
12
45
|
self.idempotency: dict[str, str] = {}
|
|
13
46
|
self.entries: list[dict[str, str]] = []
|
|
14
47
|
self.status_writes = 0
|
|
48
|
+
self.cancel_writes: list[list[tuple[str, tuple[object, ...]]]] = []
|
|
15
49
|
|
|
16
50
|
def eval(
|
|
17
51
|
self,
|
|
@@ -49,11 +83,10 @@ class FakeRedis:
|
|
|
49
83
|
|
|
50
84
|
return [request_id, 1]
|
|
51
85
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
pass
|
|
86
|
+
def pipeline(self, *, transaction: bool) -> FakePipeline:
|
|
87
|
+
if transaction:
|
|
88
|
+
raise AssertionError("status initialization should not use a transaction")
|
|
89
|
+
return FakePipeline(self)
|
|
57
90
|
|
|
58
91
|
|
|
59
92
|
def send_upload(
|
|
@@ -72,6 +105,7 @@ def send_upload(
|
|
|
72
105
|
material_title="Notes",
|
|
73
106
|
url="https://example.com/notes.pdf",
|
|
74
107
|
generate_notes=True,
|
|
108
|
+
content_hash="sha256:content-1",
|
|
75
109
|
idempotency_key=idempotency_key,
|
|
76
110
|
)
|
|
77
111
|
|
|
@@ -104,6 +138,45 @@ class IdempotentDocumentUploadTests(unittest.TestCase):
|
|
|
104
138
|
|
|
105
139
|
self.assertEqual(len(self.redis.entries), 0)
|
|
106
140
|
|
|
141
|
+
def test_content_hash_is_serialized(self) -> None:
|
|
142
|
+
send_upload(self.queue, "with-hash")
|
|
143
|
+
|
|
144
|
+
payload = json.loads(self.redis.entries[0]["payload"])
|
|
145
|
+
|
|
146
|
+
self.assertEqual(payload["contentHash"], "sha256:content-1")
|
|
147
|
+
|
|
148
|
+
def test_content_hash_remains_optional(self) -> None:
|
|
149
|
+
result = self.queue.send(
|
|
150
|
+
uploaded_file_id="file-1",
|
|
151
|
+
study_set_id="set-1",
|
|
152
|
+
user_id="user-1",
|
|
153
|
+
text_extract_s3_key="text/key",
|
|
154
|
+
pdf_s3_key="pdf/key",
|
|
155
|
+
file_type="application/pdf",
|
|
156
|
+
effective_file_type="application/pdf",
|
|
157
|
+
filename="notes.pdf",
|
|
158
|
+
material_title="Notes",
|
|
159
|
+
url="https://example.com/notes.pdf",
|
|
160
|
+
generate_notes=True,
|
|
161
|
+
idempotency_key="without-hash",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self.assertTrue(result.message_id)
|
|
165
|
+
payload = json.loads(self.redis.entries[0]["payload"])
|
|
166
|
+
self.assertNotIn("contentHash", payload)
|
|
167
|
+
|
|
168
|
+
def test_manual_cancellation_writes_scoped_tombstone(self) -> None:
|
|
169
|
+
self.queue.cancel_document("file-1")
|
|
170
|
+
|
|
171
|
+
self.assertEqual(len(self.redis.cancel_writes), 1)
|
|
172
|
+
operations = self.redis.cancel_writes[0]
|
|
173
|
+
self.assertRegex(
|
|
174
|
+
str(operations[0][1][0]),
|
|
175
|
+
r"^staging:\{document-upload\}:ctl:document:",
|
|
176
|
+
)
|
|
177
|
+
self.assertEqual(operations[0][1][-1]["mapping"]["manualCancel"], "1")
|
|
178
|
+
self.assertEqual(operations[-1][1][1], 7 * 24 * 60 * 60)
|
|
179
|
+
|
|
107
180
|
|
|
108
181
|
if __name__ == "__main__":
|
|
109
182
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
{sf_queue_sdk-0.5.0b56 → sf_queue_sdk-0.5.0b79}/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
|
|
File without changes
|