sf-queue-sdk 0.1.0__py3-none-any.whl

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.
queue_sdk/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """sf-queue Python SDK - Redis-based queue system."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from queue_sdk.client import QueueClient
6
+ from queue_sdk.types import (
7
+ BatchEmailResponse,
8
+ EmailButton,
9
+ EmailData,
10
+ EmailImage,
11
+ EmailResponse,
12
+ QueueClientConfig,
13
+ SendResult,
14
+ )
15
+
16
+ __all__ = [
17
+ "QueueClient",
18
+ "QueueClientConfig",
19
+ "EmailData",
20
+ "EmailButton",
21
+ "EmailImage",
22
+ "EmailResponse",
23
+ "BatchEmailResponse",
24
+ "SendResult",
25
+ ]
queue_sdk/client.py ADDED
@@ -0,0 +1,58 @@
1
+ """QueueClient - main entry point for the Python SDK."""
2
+
3
+ from queue_sdk.queues.email_queue import EmailQueue
4
+ from queue_sdk.redis_client import create_redis_client
5
+ from queue_sdk.types import QueueClientConfig
6
+
7
+
8
+ class QueueClient:
9
+ """Main entry point for the sf-queue Python SDK.
10
+
11
+ Initialize with Redis connection details and environment,
12
+ then use typed queue properties to send messages.
13
+
14
+ Usage:
15
+ client = QueueClient(
16
+ redis_url="redis://localhost:6379",
17
+ redis_password="your-password",
18
+ environment="staging",
19
+ )
20
+
21
+ # Fire and forget
22
+ result = client.email.send(
23
+ to="user@example.com",
24
+ preview="Welcome!",
25
+ subject="Welcome!",
26
+ paragraphs=["Hello!", "Welcome to our platform."],
27
+ )
28
+
29
+ # Send and wait for confirmation
30
+ response = client.email.send_and_wait(
31
+ to="user@example.com",
32
+ preview="Welcome!",
33
+ subject="Welcome!",
34
+ paragraphs=["Hello!"],
35
+ timeout=30,
36
+ )
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ redis_url: str,
42
+ redis_password: str,
43
+ environment: str,
44
+ ):
45
+ config = QueueClientConfig(
46
+ redis_url=redis_url,
47
+ redis_password=redis_password,
48
+ environment=environment,
49
+ )
50
+ self._redis = create_redis_client(config)
51
+ self._environment = environment
52
+
53
+ # Initialize queue instances
54
+ self.email = EmailQueue(self._redis, self._environment)
55
+
56
+ def disconnect(self) -> None:
57
+ """Close the Redis connection."""
58
+ self._redis.close()
File without changes
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: queue/email/v1/email.proto
5
+ # Protobuf Python Version: 6.33.5
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 33,
16
+ 5,
17
+ '',
18
+ 'queue/email/v1/email.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
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')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'queue.email.v1.email_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'ZJgithub.com/StudyFetch/sf-queue/packages/go/proto-go/queue/email/v1;emailv1'
35
+ _globals['_EMAILBUTTON']._serialized_start=46
36
+ _globals['_EMAILBUTTON']._serialized_end=99
37
+ _globals['_EMAILIMAGE']._serialized_start=101
38
+ _globals['_EMAILIMAGE']._serialized_end=195
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
45
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,73 @@
1
+ from google.protobuf.internal import containers as _containers
2
+ from google.protobuf import descriptor as _descriptor
3
+ from google.protobuf import message as _message
4
+ from collections.abc import Iterable as _Iterable, Mapping as _Mapping
5
+ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
6
+
7
+ DESCRIPTOR: _descriptor.FileDescriptor
8
+
9
+ class EmailButton(_message.Message):
10
+ __slots__ = ("text", "href")
11
+ TEXT_FIELD_NUMBER: _ClassVar[int]
12
+ HREF_FIELD_NUMBER: _ClassVar[int]
13
+ text: str
14
+ href: str
15
+ def __init__(self, text: _Optional[str] = ..., href: _Optional[str] = ...) -> None: ...
16
+
17
+ class EmailImage(_message.Message):
18
+ __slots__ = ("src", "alt", "width", "height")
19
+ SRC_FIELD_NUMBER: _ClassVar[int]
20
+ ALT_FIELD_NUMBER: _ClassVar[int]
21
+ WIDTH_FIELD_NUMBER: _ClassVar[int]
22
+ HEIGHT_FIELD_NUMBER: _ClassVar[int]
23
+ src: str
24
+ alt: str
25
+ width: int
26
+ height: int
27
+ def __init__(self, src: _Optional[str] = ..., alt: _Optional[str] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ...
28
+
29
+ class EmailRequest(_message.Message):
30
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image")
31
+ TO_FIELD_NUMBER: _ClassVar[int]
32
+ PREVIEW_FIELD_NUMBER: _ClassVar[int]
33
+ SUBJECT_FIELD_NUMBER: _ClassVar[int]
34
+ PARAGRAPHS_FIELD_NUMBER: _ClassVar[int]
35
+ BUTTON_FIELD_NUMBER: _ClassVar[int]
36
+ REPLY_TO_FIELD_NUMBER: _ClassVar[int]
37
+ IMAGE_FIELD_NUMBER: _ClassVar[int]
38
+ to: str
39
+ preview: str
40
+ subject: str
41
+ paragraphs: _containers.RepeatedScalarFieldContainer[str]
42
+ button: EmailButton
43
+ reply_to: str
44
+ 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: ...
46
+
47
+ class BatchEmailRequest(_message.Message):
48
+ __slots__ = ("to", "preview", "subject", "paragraphs", "button", "reply_to", "image")
49
+ TO_FIELD_NUMBER: _ClassVar[int]
50
+ PREVIEW_FIELD_NUMBER: _ClassVar[int]
51
+ SUBJECT_FIELD_NUMBER: _ClassVar[int]
52
+ PARAGRAPHS_FIELD_NUMBER: _ClassVar[int]
53
+ BUTTON_FIELD_NUMBER: _ClassVar[int]
54
+ REPLY_TO_FIELD_NUMBER: _ClassVar[int]
55
+ IMAGE_FIELD_NUMBER: _ClassVar[int]
56
+ to: _containers.RepeatedScalarFieldContainer[str]
57
+ preview: str
58
+ subject: str
59
+ paragraphs: _containers.RepeatedScalarFieldContainer[str]
60
+ button: EmailButton
61
+ reply_to: str
62
+ 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: ...
64
+
65
+ class EmailResponse(_message.Message):
66
+ __slots__ = ("success", "message_id", "error")
67
+ SUCCESS_FIELD_NUMBER: _ClassVar[int]
68
+ MESSAGE_ID_FIELD_NUMBER: _ClassVar[int]
69
+ ERROR_FIELD_NUMBER: _ClassVar[int]
70
+ success: bool
71
+ message_id: str
72
+ error: str
73
+ def __init__(self, success: _Optional[bool] = ..., message_id: _Optional[str] = ..., error: _Optional[str] = ...) -> None: ...
File without changes
@@ -0,0 +1,71 @@
1
+ """Base queue class with send/send_and_wait mechanics."""
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Optional
7
+
8
+ import redis as redis_lib
9
+
10
+ from queue_sdk.redis_client import response_key, stream_name
11
+ from queue_sdk.types import EmailResponse
12
+
13
+ DEFAULT_TIMEOUT = 30 # seconds
14
+
15
+
16
+ class BaseQueue:
17
+ """Base queue providing core send/send_and_wait mechanics for any queue type."""
18
+
19
+ def __init__(self, redis_client: redis_lib.Redis, environment: str, queue_name: str):
20
+ self._redis = redis_client
21
+ self._environment = environment
22
+ self._queue_name = queue_name
23
+ # Use hash tags for Redis Cluster slot compatibility
24
+ self._stream = stream_name(environment, "{" + queue_name + "}")
25
+
26
+ def _enqueue(self, payload: dict[str, Any]) -> str:
27
+ """Add a message to the stream and return the request ID."""
28
+ request_id = str(uuid.uuid4())
29
+ message = {
30
+ "requestId": request_id,
31
+ "payload": json.dumps(payload),
32
+ "createdAt": datetime.now(timezone.utc).isoformat(),
33
+ }
34
+
35
+ self._redis.xadd(self._stream, message)
36
+ return request_id
37
+
38
+ def _enqueue_and_wait(
39
+ self, payload: dict[str, Any], timeout: Optional[int] = None
40
+ ) -> EmailResponse:
41
+ """Add a message and wait for the response."""
42
+ request_id = self._enqueue(payload)
43
+ wait_timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
44
+
45
+ key = response_key(self._environment, self._queue_name, request_id)
46
+
47
+ # BRPOP blocks until a response is pushed or timeout
48
+ result = self._redis.brpop(key, timeout=wait_timeout)
49
+
50
+ if result is None:
51
+ return EmailResponse(
52
+ success=False,
53
+ message_id=request_id,
54
+ error=f"Timeout waiting for response after {wait_timeout}s",
55
+ )
56
+
57
+ try:
58
+ # result is (key, value) tuple
59
+ response_data = json.loads(result[1])
60
+ return EmailResponse(
61
+ success=response_data.get("success", False),
62
+ message_id=response_data.get("messageId", request_id),
63
+ error=response_data.get("error"),
64
+ processed_at=response_data.get("processedAt"),
65
+ )
66
+ except (json.JSONDecodeError, KeyError):
67
+ return EmailResponse(
68
+ success=False,
69
+ message_id=request_id,
70
+ error="Failed to parse response from queue",
71
+ )
@@ -0,0 +1,161 @@
1
+ """Email 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.sanitize import sanitize, sanitize_list
9
+ from queue_sdk.types import (
10
+ BatchEmailResponse,
11
+ EmailResponse,
12
+ SendResult,
13
+ )
14
+
15
+
16
+ class EmailQueue(BaseQueue):
17
+ """Typed methods for sending emails via the queue."""
18
+
19
+ def __init__(self, redis_client: redis_lib.Redis, environment: str):
20
+ super().__init__(redis_client, environment, "email")
21
+
22
+ def send(
23
+ self,
24
+ to: str,
25
+ preview: str,
26
+ subject: str,
27
+ paragraphs: list[str],
28
+ button: Optional[dict[str, str]] = None,
29
+ reply_to: Optional[str] = None,
30
+ image: Optional[dict[str, Any]] = None,
31
+ ) -> SendResult:
32
+ """Enqueue an email for processing (fire and forget)."""
33
+ payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
34
+ message_id = self._enqueue(payload)
35
+ return SendResult(message_id=message_id)
36
+
37
+ def send_and_wait(
38
+ self,
39
+ to: str,
40
+ preview: str,
41
+ subject: str,
42
+ paragraphs: list[str],
43
+ button: Optional[dict[str, str]] = None,
44
+ reply_to: Optional[str] = None,
45
+ image: Optional[dict[str, Any]] = None,
46
+ timeout: Optional[int] = None,
47
+ ) -> EmailResponse:
48
+ """Enqueue an email and wait for processing confirmation."""
49
+ payload = self._build_payload(to, preview, subject, paragraphs, button, reply_to, image)
50
+ return self._enqueue_and_wait(payload, timeout)
51
+
52
+ def send_batch(
53
+ self,
54
+ to: list[str],
55
+ preview: str,
56
+ subject: str,
57
+ paragraphs: list[str],
58
+ button: Optional[dict[str, str]] = None,
59
+ reply_to: Optional[str] = None,
60
+ image: Optional[dict[str, Any]] = None,
61
+ ) -> SendResult:
62
+ """Enqueue a batch email for processing (fire and forget)."""
63
+ self._validate_batch(to)
64
+ payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
65
+ message_id = self._enqueue(payload)
66
+ return SendResult(message_id=message_id)
67
+
68
+ def send_batch_and_wait(
69
+ self,
70
+ to: list[str],
71
+ preview: str,
72
+ subject: str,
73
+ paragraphs: list[str],
74
+ button: Optional[dict[str, str]] = None,
75
+ reply_to: Optional[str] = None,
76
+ image: Optional[dict[str, Any]] = None,
77
+ timeout: Optional[int] = None,
78
+ ) -> BatchEmailResponse:
79
+ """Enqueue a batch email and wait for processing confirmation."""
80
+ self._validate_batch(to)
81
+ payload = self._build_batch_payload(to, preview, subject, paragraphs, button, reply_to, image)
82
+ response = self._enqueue_and_wait(payload, timeout)
83
+
84
+ return BatchEmailResponse(
85
+ success=response.success,
86
+ message_id=response.message_id,
87
+ error=response.error,
88
+ processed_at=response.processed_at,
89
+ total=getattr(response, "total", len(to)),
90
+ successful=getattr(response, "successful", len(to) if response.success else 0),
91
+ failed=getattr(response, "failed", 0 if response.success else len(to)),
92
+ )
93
+
94
+ def _build_payload(
95
+ self,
96
+ to: str,
97
+ preview: str,
98
+ subject: str,
99
+ paragraphs: list[str],
100
+ button: Optional[dict[str, str]],
101
+ reply_to: Optional[str],
102
+ image: Optional[dict[str, Any]],
103
+ ) -> dict[str, Any]:
104
+ """Build and sanitize the email payload."""
105
+ payload: dict[str, Any] = {
106
+ "to": to,
107
+ "preview": sanitize(preview),
108
+ "subject": sanitize(subject),
109
+ "paragraphs": sanitize_list(paragraphs),
110
+ }
111
+
112
+ if button:
113
+ payload["button"] = {
114
+ "text": sanitize(button["text"]),
115
+ "href": button["href"],
116
+ }
117
+
118
+ if reply_to:
119
+ payload["replyTo"] = reply_to
120
+
121
+ if image:
122
+ payload["image"] = {
123
+ "src": image.get("src", ""),
124
+ "alt": image.get("alt", ""),
125
+ "width": image.get("width"),
126
+ "height": image.get("height"),
127
+ }
128
+
129
+ return payload
130
+
131
+ def _build_batch_payload(
132
+ self,
133
+ to: list[str],
134
+ preview: str,
135
+ subject: str,
136
+ paragraphs: list[str],
137
+ button: Optional[dict[str, str]],
138
+ reply_to: Optional[str],
139
+ image: Optional[dict[str, Any]],
140
+ ) -> dict[str, Any]:
141
+ """Build and sanitize the batch email payload."""
142
+ payload = self._build_payload(
143
+ to="", # not used for batch
144
+ preview=preview,
145
+ subject=subject,
146
+ paragraphs=paragraphs,
147
+ button=button,
148
+ reply_to=reply_to,
149
+ image=image,
150
+ )
151
+ payload["to"] = to
152
+ payload["batch"] = True
153
+ return payload
154
+
155
+ @staticmethod
156
+ def _validate_batch(to: list[str]) -> None:
157
+ """Validate batch constraints."""
158
+ if len(to) > 100:
159
+ raise ValueError("Maximum 100 recipients allowed per batch request")
160
+ if len(to) == 0:
161
+ raise ValueError("At least one recipient is required")
@@ -0,0 +1,51 @@
1
+ """Redis connection factory."""
2
+
3
+ from urllib.parse import urlparse
4
+
5
+ import redis
6
+
7
+ from queue_sdk.types import QueueClientConfig
8
+
9
+
10
+ def create_redis_client(config: QueueClientConfig) -> redis.Redis:
11
+ """Create a Redis client from the SDK config."""
12
+ url = config.redis_url
13
+ password = config.redis_password
14
+
15
+ # Full URL format (redis://... or rediss://...)
16
+ if url.startswith("redis://") or url.startswith("rediss://"):
17
+ parsed = urlparse(url)
18
+ effective_password = password or parsed.password or None
19
+ return redis.Redis(
20
+ host=parsed.hostname or "localhost",
21
+ port=parsed.port or 6379,
22
+ password=effective_password,
23
+ decode_responses=True,
24
+ )
25
+
26
+ # Simple host:port format
27
+ parts = url.split(":")
28
+ host = parts[0] if parts[0] else "localhost"
29
+ port = int(parts[1]) if len(parts) > 1 else 6379
30
+
31
+ return redis.Redis(
32
+ host=host,
33
+ port=port,
34
+ password=password or None,
35
+ decode_responses=True,
36
+ )
37
+
38
+
39
+ def stream_name(env: str, base: str) -> str:
40
+ """Return the full stream name with environment prefix."""
41
+ if not env:
42
+ return base
43
+ return f"{env}:{base}"
44
+
45
+
46
+ def response_key(env: str, queue: str, request_id: str) -> str:
47
+ """Return the response list key for a given queue and request ID."""
48
+ key = f"{queue}:response:{request_id}"
49
+ if not env:
50
+ return key
51
+ return f"{env}:{key}"
queue_sdk/sanitize.py ADDED
@@ -0,0 +1,15 @@
1
+ """Basic HTML sanitization for email content."""
2
+
3
+ import re
4
+
5
+ _HTML_TAG_REGEX = re.compile(r"<[^>]*>")
6
+
7
+
8
+ def sanitize(text: str) -> str:
9
+ """Strip HTML tags from a string."""
10
+ return _HTML_TAG_REGEX.sub("", text)
11
+
12
+
13
+ def sanitize_list(texts: list[str]) -> list[str]:
14
+ """Sanitize each string in a list."""
15
+ return [sanitize(t) for t in texts]
queue_sdk/types.py ADDED
@@ -0,0 +1,87 @@
1
+ """Type definitions for the queue SDK."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class QueueClientConfig:
9
+ """Configuration for the QueueClient."""
10
+
11
+ redis_url: str
12
+ redis_password: str
13
+ environment: str
14
+
15
+
16
+ @dataclass
17
+ class EmailButton:
18
+ """CTA button for email."""
19
+
20
+ text: str
21
+ href: str
22
+
23
+
24
+ @dataclass
25
+ class EmailImage:
26
+ """Optional image for email."""
27
+
28
+ src: str
29
+ alt: str = ""
30
+ width: Optional[int] = None
31
+ height: Optional[int] = None
32
+
33
+
34
+ @dataclass
35
+ class EmailData:
36
+ """Data for a single email."""
37
+
38
+ to: str
39
+ preview: str
40
+ subject: str
41
+ paragraphs: list[str]
42
+ button: Optional[EmailButton] = None
43
+ reply_to: Optional[str] = None
44
+ image: Optional[EmailImage] = None
45
+
46
+
47
+ @dataclass
48
+ class BatchEmailData:
49
+ """Data for a batch email (same content to multiple recipients)."""
50
+
51
+ to: list[str]
52
+ preview: str
53
+ subject: str
54
+ paragraphs: list[str]
55
+ button: Optional[EmailButton] = None
56
+ reply_to: Optional[str] = None
57
+ image: Optional[EmailImage] = None
58
+
59
+
60
+ @dataclass
61
+ class SendResult:
62
+ """Result of a fire-and-forget send."""
63
+
64
+ message_id: str
65
+
66
+
67
+ @dataclass
68
+ class EmailResponse:
69
+ """Response from a processed email."""
70
+
71
+ success: bool
72
+ message_id: str
73
+ error: Optional[str] = None
74
+ processed_at: Optional[str] = None
75
+
76
+
77
+ @dataclass
78
+ class BatchEmailResponse:
79
+ """Response from a processed batch email."""
80
+
81
+ success: bool
82
+ message_id: str
83
+ total: int
84
+ successful: int
85
+ failed: int
86
+ error: Optional[str] = None
87
+ processed_at: Optional[str] = None
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: sf-queue-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for sf-queue - Redis-based queue system
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: redis>=5.0.0
@@ -0,0 +1,14 @@
1
+ queue_sdk/__init__.py,sha256=x-e5r6aQKhRVrOESKXU26RkXiLk5G1jFOTGA3gRwSlA,459
2
+ queue_sdk/client.py,sha256=VO3-4ykjCQbu9L20NM4nZXKAijBTUziaCH8aLJ8wwaw,1656
3
+ queue_sdk/redis_client.py,sha256=piToSab1T-nt0cAG2ZWSXbgeyJE7o1MlsMX3u3tGzFg,1441
4
+ queue_sdk/sanitize.py,sha256=yUPDvcu531b_guYgYmI8H1jDf2Q72Z7vCetzmHoRHwQ,352
5
+ queue_sdk/types.py,sha256=pR_AhDYHE1NNE0k_GmwwQbaM0E_0YvIPr3_NAnR_ucs,1596
6
+ queue_sdk/generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ queue_sdk/generated/queue/email/v1/email_pb2.py,sha256=9VsYHPDrTRNPpPZjGEuE3l_jeIriV7o8s-0qRXsj3kw,3255
8
+ queue_sdk/generated/queue/email/v1/email_pb2.pyi,sha256=ilMIcDo-YCb3SOCWgiNl4vxMAaOaKRZcPEkMYzVFe7Y,3317
9
+ queue_sdk/queues/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ queue_sdk/queues/base_queue.py,sha256=eGrlN2xRwaTjEhR9BIElDZ5peLYmzwKIMvUp-4ZutBE,2524
11
+ queue_sdk/queues/email_queue.py,sha256=COqQ0UWBDZSwQ2EzQnkoJMlexbq2IX-269Ef9DAEoi0,5275
12
+ sf_queue_sdk-0.1.0.dist-info/METADATA,sha256=G53ZP-x1M8YJMXfBqfaWjc4FY8XaU9wBKwrrCymsuIk,168
13
+ sf_queue_sdk-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ sf_queue_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any