morp 7.0.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.
- morp/__init__.py +23 -0
- morp/compat.py +4 -0
- morp/config.py +173 -0
- morp/exception.py +35 -0
- morp/interface/__init__.py +81 -0
- morp/interface/base.py +441 -0
- morp/interface/dropfile.py +160 -0
- morp/interface/postgres.py +368 -0
- morp/interface/sqs.py +396 -0
- morp/message.py +354 -0
- morp-7.0.0.dist-info/METADATA +212 -0
- morp-7.0.0.dist-info/RECORD +15 -0
- morp-7.0.0.dist-info/WHEEL +5 -0
- morp-7.0.0.dist-info/licenses/LICENSE.txt +21 -0
- morp-7.0.0.dist-info/top_level.txt +2 -0
morp/message.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from contextlib import asynccontextmanager, AbstractAsyncContextManager
|
|
3
|
+
import logging
|
|
4
|
+
import inspect
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from datatypes import (
|
|
8
|
+
ReflectClass,
|
|
9
|
+
ReflectName,
|
|
10
|
+
ReflectType,
|
|
11
|
+
make_dict,
|
|
12
|
+
classproperty,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .compat import *
|
|
16
|
+
from .interface import get_interface
|
|
17
|
+
from .exception import ReleaseMessage, AckMessage
|
|
18
|
+
from .config import environ
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Message(object):
|
|
25
|
+
"""
|
|
26
|
+
this is the base class for sending and handling a message
|
|
27
|
+
|
|
28
|
+
to add a new message to your application, just subclass this class
|
|
29
|
+
|
|
30
|
+
:example:
|
|
31
|
+
import morp
|
|
32
|
+
|
|
33
|
+
class CustomMessage(morp.Message):
|
|
34
|
+
# message fields
|
|
35
|
+
foo: int
|
|
36
|
+
bar: str
|
|
37
|
+
|
|
38
|
+
# class fields start with an underscore
|
|
39
|
+
_ignored: bool = False
|
|
40
|
+
|
|
41
|
+
def handle(self):
|
|
42
|
+
# target will be called when the message is consumed using
|
|
43
|
+
# the CustomMessage.handle method
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
m1 = CustomMessage(foo=1, bar="one")
|
|
47
|
+
await m1.send() # m1 is sent with foo and bar fields
|
|
48
|
+
|
|
49
|
+
m2 = await CustomMessage.create(foo=2, bar="two")
|
|
50
|
+
# m2 was created and sent with foo and bar fields
|
|
51
|
+
|
|
52
|
+
await CustomMessage.process(2)
|
|
53
|
+
# both m1 and m2 were consumed and their .target methods called
|
|
54
|
+
|
|
55
|
+
By default, all subclasses will go to the same queue and then when the
|
|
56
|
+
queue is consumed the correct child class will be created and consume the
|
|
57
|
+
message with its `.handle` method.
|
|
58
|
+
|
|
59
|
+
If you would like your subclass to use a different queue then just set the
|
|
60
|
+
`._name` property on the class and it will use a different queue
|
|
61
|
+
"""
|
|
62
|
+
_connection_name: str = ""
|
|
63
|
+
"""the name of the connection to use to retrieve the interface"""
|
|
64
|
+
|
|
65
|
+
_name: str = "morp-messages"
|
|
66
|
+
"""The queue name, see .get_name()"""
|
|
67
|
+
|
|
68
|
+
_classpath_key: str = "_classpath"
|
|
69
|
+
"""The key that will be used to hold the Message's child class's full
|
|
70
|
+
classpath, see `._to_interface` and `.from_interface`"""
|
|
71
|
+
|
|
72
|
+
_message_classes: dict = {}
|
|
73
|
+
"""Holds all the children message classes. See `__init_subclass__`"""
|
|
74
|
+
|
|
75
|
+
@classproperty
|
|
76
|
+
def interface(cls):
|
|
77
|
+
return get_interface(cls._connection_name)
|
|
78
|
+
|
|
79
|
+
@classproperty
|
|
80
|
+
def schema(cls):
|
|
81
|
+
schema = {}
|
|
82
|
+
|
|
83
|
+
for field_name, field_type in typing.get_type_hints(cls).items():
|
|
84
|
+
if field_name.startswith("_"):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
schema[field_name] = ReflectType(field_type)
|
|
88
|
+
|
|
89
|
+
# cache the value so we don't need to generate it again
|
|
90
|
+
cls.schema = schema
|
|
91
|
+
return cls.schema
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def fields(self):
|
|
95
|
+
fields = {}
|
|
96
|
+
|
|
97
|
+
for field_name in self.schema.keys():
|
|
98
|
+
v = getattr(self, field_name, typing.NoReturn)
|
|
99
|
+
if v is not typing.NoReturn:
|
|
100
|
+
fields[field_name] = v
|
|
101
|
+
|
|
102
|
+
return fields
|
|
103
|
+
|
|
104
|
+
def __init__(self, fields=None, **fields_kwargs):
|
|
105
|
+
fields = make_dict(fields, fields_kwargs)
|
|
106
|
+
self._from_interface(fields)
|
|
107
|
+
|
|
108
|
+
def __init_subclass__(cls):
|
|
109
|
+
"""When a child class is loaded into memory it will be saved into
|
|
110
|
+
.orm_classes, this way every orm class knows about all the other orm
|
|
111
|
+
classes, this is the method that makes that possible magically
|
|
112
|
+
|
|
113
|
+
https://peps.python.org/pep-0487/
|
|
114
|
+
"""
|
|
115
|
+
cls._message_classes[f"{cls.__module__}:{cls.__qualname__}"] = cls
|
|
116
|
+
|
|
117
|
+
def __contains__(self, field_name):
|
|
118
|
+
v = getattr(self, field_name, typing.NoReturn)
|
|
119
|
+
return v is not typing.NoReturn
|
|
120
|
+
#return hasattr(self, key)
|
|
121
|
+
#return key in self.fields
|
|
122
|
+
|
|
123
|
+
async def send(self, **kwargs):
|
|
124
|
+
"""send the message using the configured interface for this class
|
|
125
|
+
|
|
126
|
+
:param **kwargs:
|
|
127
|
+
:keyword delay_seconds: int, how many seconds before the message can
|
|
128
|
+
be processed. The max value is interface specific, (eg, it can
|
|
129
|
+
only be 900s max (15 minutes) per SQS docs)
|
|
130
|
+
"""
|
|
131
|
+
queue_off = environ.DISABLED
|
|
132
|
+
name = self.get_name()
|
|
133
|
+
fields = self._to_interface()
|
|
134
|
+
if queue_off:
|
|
135
|
+
logger.warning("DISABLED - Would have sent {} to {}".format(
|
|
136
|
+
fields,
|
|
137
|
+
name,
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
logger.info("Sending message with '{}' keys to '{}'".format(
|
|
142
|
+
"', '".join(fields.keys()),
|
|
143
|
+
name
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
hydrate_fields = await self.interface.send(
|
|
147
|
+
name=name,
|
|
148
|
+
fields=fields,
|
|
149
|
+
**kwargs,
|
|
150
|
+
)
|
|
151
|
+
# we mimic hydration
|
|
152
|
+
self._from_interface(hydrate_fields)
|
|
153
|
+
self._hydrate_fields = hydrate_fields
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def get_name(cls):
|
|
157
|
+
"""This is what's used as the official queue name, it takes `.name`
|
|
158
|
+
and combines it with the `MORP_PREFIX` environment variable
|
|
159
|
+
|
|
160
|
+
:returns: str, the queue name
|
|
161
|
+
"""
|
|
162
|
+
name = cls._name
|
|
163
|
+
if env_name := environ.PREFIX:
|
|
164
|
+
name = "{}-{}".format(env_name, name)
|
|
165
|
+
return name
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
async def process(cls, count=0, **kwargs):
|
|
169
|
+
"""wait for messages to come in and handle them by calling the incoming
|
|
170
|
+
message's `handle` method
|
|
171
|
+
|
|
172
|
+
:example:
|
|
173
|
+
# handle 10 messages by receiving them and calling `handle` on the
|
|
174
|
+
# message instance
|
|
175
|
+
Message.process(10)
|
|
176
|
+
|
|
177
|
+
:param count: int, if you only want to handle N messages, pass in count
|
|
178
|
+
:param **kwargs: any other params will get passed to underlying
|
|
179
|
+
`._recv` methods
|
|
180
|
+
"""
|
|
181
|
+
max_count = count
|
|
182
|
+
count = 0
|
|
183
|
+
while not max_count or count < max_count:
|
|
184
|
+
count += 1
|
|
185
|
+
logger.debug("Receiving {}/{}".format(
|
|
186
|
+
count,
|
|
187
|
+
max_count if max_count else "Infinity"
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
async with cls._recv(**kwargs) as m:
|
|
191
|
+
r = m.handle()
|
|
192
|
+
while inspect.iscoroutine(r):
|
|
193
|
+
r = await r
|
|
194
|
+
|
|
195
|
+
if r is False:
|
|
196
|
+
raise ReleaseMessage()
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
@asynccontextmanager
|
|
200
|
+
async def _recv(cls, **kwargs) -> AbstractAsyncContextManager:
|
|
201
|
+
"""Internal context manager that receives a message to be processed
|
|
202
|
+
|
|
203
|
+
Called from `.process` to receive messages and repeatedly calls
|
|
204
|
+
`._recv_for` to actually receive a message
|
|
205
|
+
|
|
206
|
+
:keyword timeout: int, how long to wait before yielding None
|
|
207
|
+
"""
|
|
208
|
+
m = None
|
|
209
|
+
# 20 is the max long polling timeout per Amazon
|
|
210
|
+
kwargs.setdefault('timeout', 20)
|
|
211
|
+
while not m:
|
|
212
|
+
async with cls._recv_for(**kwargs) as m:
|
|
213
|
+
if m:
|
|
214
|
+
yield m
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
@asynccontextmanager
|
|
218
|
+
async def _recv_for(
|
|
219
|
+
cls,
|
|
220
|
+
timeout: int|float,
|
|
221
|
+
**kwargs,
|
|
222
|
+
) -> AbstractAsyncContextManager:
|
|
223
|
+
"""Internal context manager. Try and receive a message, return None
|
|
224
|
+
if a message is not received within timeout
|
|
225
|
+
|
|
226
|
+
:param timeout: float|int, how many seconds before yielding None
|
|
227
|
+
:returns: generator[Message]
|
|
228
|
+
"""
|
|
229
|
+
i = cls.interface
|
|
230
|
+
name = cls.get_name()
|
|
231
|
+
ack_on_recv = kwargs.pop('ack_on_recv', False)
|
|
232
|
+
logger.debug(
|
|
233
|
+
"Waiting to receive on {} for {} seconds".format(name, timeout)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async with i.connection(**kwargs) as connection:
|
|
237
|
+
kwargs["connection"] = connection
|
|
238
|
+
|
|
239
|
+
fields = await i.recv(name, timeout=timeout, **kwargs)
|
|
240
|
+
if fields:
|
|
241
|
+
try:
|
|
242
|
+
yield cls._hydrate(fields)
|
|
243
|
+
|
|
244
|
+
except ReleaseMessage as e:
|
|
245
|
+
await i.release(
|
|
246
|
+
name,
|
|
247
|
+
fields,
|
|
248
|
+
delay_seconds=e.delay_seconds,
|
|
249
|
+
**kwargs
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
except AckMessage as e:
|
|
253
|
+
await i.ack(name, fields, **kwargs)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
if ack_on_recv:
|
|
257
|
+
await i.ack(name, fields, **kwargs)
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
await i.release(name, fields, **kwargs)
|
|
261
|
+
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
else:
|
|
265
|
+
await i.ack(name, fields, **kwargs)
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
yield None
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
async def create(cls, *args, **kwargs):
|
|
272
|
+
"""create an instance of cls with the passed in fields and send it off
|
|
273
|
+
|
|
274
|
+
Since this passed *args and **kwargs directly to .__init__, you can
|
|
275
|
+
override the .__init__ method and customize it and this method will
|
|
276
|
+
inherit the child class's changes. And the signature of this method
|
|
277
|
+
should always match .__init__
|
|
278
|
+
|
|
279
|
+
:param *args: list[Any], passed directly to .__init__
|
|
280
|
+
:param **kwargs: dict[str, Any], passed directly to .__init__
|
|
281
|
+
"""
|
|
282
|
+
connection = kwargs.pop("connection", None)
|
|
283
|
+
instance = cls(*args, **kwargs)
|
|
284
|
+
await instance.send(connection=connection)
|
|
285
|
+
return instance
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
async def count(cls):
|
|
289
|
+
"""how many messages total (approximately) are in the whole message
|
|
290
|
+
queue"""
|
|
291
|
+
n = cls.get_name()
|
|
292
|
+
return await cls.interface.count(n)
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def _hydrate(cls, fields):
|
|
296
|
+
"""This is used by the interface to populate an instance with
|
|
297
|
+
information received from the interface
|
|
298
|
+
|
|
299
|
+
:param fields: InterfaceMessage, the message freshly received from
|
|
300
|
+
the interface, see Interface.create_imessage()
|
|
301
|
+
"""
|
|
302
|
+
message_class = cls
|
|
303
|
+
if cls is Message:
|
|
304
|
+
# When a generic Message instance is used to consume messages it
|
|
305
|
+
# will use the passed in classpath to create the correct Message
|
|
306
|
+
# child
|
|
307
|
+
rn = ReflectName(fields.get(cls._classpath_key))
|
|
308
|
+
message_class = rn.get_class()
|
|
309
|
+
|
|
310
|
+
instance = message_class()
|
|
311
|
+
instance._from_interface(fields)
|
|
312
|
+
instance._hydrate_fields = fields
|
|
313
|
+
|
|
314
|
+
return instance
|
|
315
|
+
|
|
316
|
+
def _to_interface(self):
|
|
317
|
+
"""When sending a message to the interface this method will be called
|
|
318
|
+
|
|
319
|
+
:returns: dict, the fields
|
|
320
|
+
"""
|
|
321
|
+
fields = {}
|
|
322
|
+
schema = self.schema
|
|
323
|
+
for field_name, rt in schema.items():
|
|
324
|
+
fields[field_name] = rt.cast(getattr(self, field_name))
|
|
325
|
+
|
|
326
|
+
if self._classpath_key not in fields:
|
|
327
|
+
fields[self._classpath_key] = ReflectClass(self).classpath
|
|
328
|
+
|
|
329
|
+
return fields
|
|
330
|
+
|
|
331
|
+
def _from_interface(self, fields):
|
|
332
|
+
"""When receiving a message from the interface this method will
|
|
333
|
+
be called
|
|
334
|
+
|
|
335
|
+
you can see it in action with `._hydrate`
|
|
336
|
+
|
|
337
|
+
:param fields: dict, the fields received from the interface
|
|
338
|
+
"""
|
|
339
|
+
for field_name in self.schema.keys():
|
|
340
|
+
if field_name in fields:
|
|
341
|
+
setattr(self, field_name, fields[field_name])
|
|
342
|
+
|
|
343
|
+
async def handle(self) -> bool|None:
|
|
344
|
+
"""This method will be called from `.process` and can handle any
|
|
345
|
+
processing of the message, it should be defined in the child classes
|
|
346
|
+
|
|
347
|
+
:returns:
|
|
348
|
+
- if False, the message will be released back to be processed
|
|
349
|
+
again
|
|
350
|
+
- any other value is considered a success and the message is
|
|
351
|
+
acked/consumed
|
|
352
|
+
"""
|
|
353
|
+
raise NotImplementedError()
|
|
354
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morp
|
|
3
|
+
Version: 7.0.0
|
|
4
|
+
Summary: Send and receive messages without thinking about it
|
|
5
|
+
Author-email: Jay Marcyes <jay@marcyes.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, http://github.com/Jaymon/morp
|
|
8
|
+
Project-URL: Repository, https://github.com/Jaymon/morp
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Database
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE.txt
|
|
20
|
+
Requires-Dist: dsnparse
|
|
21
|
+
Requires-Dist: datatypes
|
|
22
|
+
Provides-Extra: tests
|
|
23
|
+
Requires-Dist: testdata; extra == "tests"
|
|
24
|
+
Provides-Extra: postgres
|
|
25
|
+
Requires-Dist: psycopg; extra == "postgres"
|
|
26
|
+
Provides-Extra: sqs
|
|
27
|
+
Requires-Dist: boto3; extra == "sqs"
|
|
28
|
+
Provides-Extra: encryption
|
|
29
|
+
Requires-Dist: cryptography; extra == "encryption"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Morp
|
|
33
|
+
|
|
34
|
+
Simple message processing without really thinking about it. Morp can use dropfiles (simple text files), Postgres, and [Amazon SQS](http://aws.amazon.com/sqs/).
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Use pip to install the latest stable version:
|
|
40
|
+
|
|
41
|
+
pip install morp
|
|
42
|
+
|
|
43
|
+
Morp only supports the dropfiles interface out of the box, you'll need to install certain dependencies depending on what interface you want to use:
|
|
44
|
+
|
|
45
|
+
pip install morp[sqs]
|
|
46
|
+
pip install morp[postgres]
|
|
47
|
+
|
|
48
|
+
To install the development version:
|
|
49
|
+
|
|
50
|
+
pip install -U "git+https://github.com/Jaymon/morp#egg=morp"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## 1 Minute Getting Started
|
|
54
|
+
|
|
55
|
+
Send and receive a `Foo` message.
|
|
56
|
+
|
|
57
|
+
First, let's set our environment variable to use dropfiles (local files suitable for development and prototyping) interface:
|
|
58
|
+
|
|
59
|
+
export MORP_DSN=dropfile:${TMPDIR}
|
|
60
|
+
|
|
61
|
+
Then, let's create three files in our working directory:
|
|
62
|
+
|
|
63
|
+
* `tasks.py` - We'll define our `Message` classes here.
|
|
64
|
+
* `send.py` - We'll send messages from this script.
|
|
65
|
+
* `recv.py` - We'll receive messages from this script.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Let's create our `Message` class in `tasks.py`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# tasks.py
|
|
72
|
+
from morp import Message
|
|
73
|
+
|
|
74
|
+
class Foo(Message):
|
|
75
|
+
some_field: int
|
|
76
|
+
some_other_field: str
|
|
77
|
+
|
|
78
|
+
def handle(self):
|
|
79
|
+
# this will be run when a Foo message is consumed
|
|
80
|
+
print(self.fields)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Now, let's flesh out our `recv.py` file:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# recv.py
|
|
87
|
+
|
|
88
|
+
import asyncio
|
|
89
|
+
|
|
90
|
+
# import our Foo message class from our tasks.py file
|
|
91
|
+
from tasks import Foo
|
|
92
|
+
|
|
93
|
+
# Foo's `process` method will call `Foo.handle` for each Foo instance received
|
|
94
|
+
asyncio.run(Foo.process())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
And start it up:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
$ python recv.py
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
Finally, let's send some messages by fleshing out `send.py`:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# send.py
|
|
108
|
+
|
|
109
|
+
import asyncio
|
|
110
|
+
|
|
111
|
+
from tasks import Foo
|
|
112
|
+
|
|
113
|
+
async def send_messages():
|
|
114
|
+
# create a message and send it manually
|
|
115
|
+
f = Foo()
|
|
116
|
+
f.some_field = 1
|
|
117
|
+
f.some_other_field = "one"
|
|
118
|
+
f.ignored_field = True
|
|
119
|
+
await f.send()
|
|
120
|
+
|
|
121
|
+
# quickly send a message
|
|
122
|
+
await Foo.create(
|
|
123
|
+
some_field=2,
|
|
124
|
+
some_other_field="two",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
asyncio.run(send_messages())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
And running it in a separate shell from the shell running our `recv.py` script (it should send two messages):
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
$ python send.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
That's it! Our running `recv.py` script should've received the messages we sent when we ran our `send.py` script.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## DSN
|
|
140
|
+
|
|
141
|
+
You configure your connection using a dsn in the form:
|
|
142
|
+
|
|
143
|
+
InterfaceName://username:password@host:port/path?param1=value1¶m2=value2
|
|
144
|
+
|
|
145
|
+
So, to connect to [Amazon SQS](http://aws.amazon.com/sqs/), you would do:
|
|
146
|
+
|
|
147
|
+
sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@
|
|
148
|
+
|
|
149
|
+
You can also override some default values like `region` and `read_lock`:
|
|
150
|
+
|
|
151
|
+
sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@?region=${AWS_DEFAULT_REGION}&read_lock=120
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### Serializers
|
|
155
|
+
|
|
156
|
+
* `pickle` (default)
|
|
157
|
+
* `json`
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
MORP_DSN="sqs://x:x@?serializer=json"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Encryption
|
|
165
|
+
|
|
166
|
+
You might need to install some dependencies:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
pip install morp[encryption]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If you would like to encrypt all your messages, you can pass in a `key` argument to your DSN and Morp will take care of encrypting and decrypting the messages for you transparently.
|
|
173
|
+
|
|
174
|
+
Let's just modify our DSN to pass in our key:
|
|
175
|
+
|
|
176
|
+
sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@?key=jy4XWRuEsrH98RD2VeLG62uVLCPWpdUh
|
|
177
|
+
|
|
178
|
+
That's it, every message will now be encrypted on send and decrypted on receive. If you're using SQS you can also use [Amazon's key management service](https://github.com/Jaymon/morp/blob/master/docs/KMS.md) to handle the encryption for you.
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
## Environment configuration
|
|
182
|
+
|
|
183
|
+
### MORP_DISABLED
|
|
184
|
+
|
|
185
|
+
By default every message will be sent, if you just want to test functionality without actually sending the message you can set this environment variable to turn off all the queues.
|
|
186
|
+
|
|
187
|
+
MORP_DISABLED = 1 # queue is off
|
|
188
|
+
MORP_DISABLED = 0 # queue is on
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
### MORP_PREFIX
|
|
192
|
+
|
|
193
|
+
If you would like to have your queue names prefixed with something (eg, `prod` or `dev`) then you can set this environment variable and it will be prefixed to the queue name.
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
### MORP_DSN
|
|
197
|
+
|
|
198
|
+
Set this environment variable with your connection DSN so Morp can automatically configure itself when the interface is first requested.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## FAQ
|
|
202
|
+
|
|
203
|
+
### I would like to have multiple queues
|
|
204
|
+
|
|
205
|
+
By default, Morp will send any message from any `morp.Message` derived class to `Message.get_name()`, you can override this behavior by giving your child class a `._name` property:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from morp import Message
|
|
209
|
+
|
|
210
|
+
class childMsg(Message):
|
|
211
|
+
_name = "custom-queue-name"
|
|
212
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
morp/__init__.py,sha256=2V8Qep-iSh73325gx-hqAwRJPh5CcpWb7uPhAB6iBCw,347
|
|
2
|
+
morp/compat.py,sha256=KPZYveHPyXYU4fKeU3fQuvBSFi-mcHK7Uqk9O_kNA-k,97
|
|
3
|
+
morp/config.py,sha256=kifJp-KL_tdDHeu5V17o25f95boQZtEpg3fR9RvOSio,4854
|
|
4
|
+
morp/exception.py,sha256=9CSZWYVNyqt7gxqApG4k3YXzrbmy_EJ_EPqw_SM8Wpk,1022
|
|
5
|
+
morp/message.py,sha256=B2ZLt83lzp-CN3dA0OIxzCegeKgYIwZ3t_FM41hW67w,11406
|
|
6
|
+
morp/interface/__init__.py,sha256=EMrMTF84Rp9V6KlnTaw4uYouLDf6jtb26CwGRX7zvts,2674
|
|
7
|
+
morp/interface/base.py,sha256=fi__yOjKw6J311BPoYAHo9RZ0Yi9MpJ5ZTUg7nZbE1M,14813
|
|
8
|
+
morp/interface/dropfile.py,sha256=oQhUVsTHIJlq_bR53SJ9W-5YZI7JOt_2FgEbx5Uc-AM,5191
|
|
9
|
+
morp/interface/postgres.py,sha256=Ov8NUPEhk63fXa5CZbXueKdmYXoIryfVZwdSQcJRnb8,12415
|
|
10
|
+
morp/interface/sqs.py,sha256=vvUdSVNqSGeM9nrMGUAPX1RVj9kZRMYet0xLht89ws4,14129
|
|
11
|
+
morp-7.0.0.dist-info/licenses/LICENSE.txt,sha256=NdwjhMrenM_sXvd3REQVjswgOw-pOITlnHCNLVp4AwI,1079
|
|
12
|
+
morp-7.0.0.dist-info/METADATA,sha256=gANIuXGUvMNlAKCZTbU6jZKCdsG3HU-Dt78N6sk9HT4,5586
|
|
13
|
+
morp-7.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
morp-7.0.0.dist-info/top_level.txt,sha256=GjC8bZE2FMSjNYMuYb-lrYy42OkOytWTV2pokuwpfDM,10
|
|
15
|
+
morp-7.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013+ Jay Marcyes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|