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/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
DsnConnection,
|
|
5
|
+
Connection,
|
|
6
|
+
)
|
|
7
|
+
from .interface import (
|
|
8
|
+
get_interface,
|
|
9
|
+
set_interface,
|
|
10
|
+
get_interfaces,
|
|
11
|
+
configure_environ,
|
|
12
|
+
find_environ,
|
|
13
|
+
)
|
|
14
|
+
from .message import Message
|
|
15
|
+
from .exception import (
|
|
16
|
+
InterfaceError,
|
|
17
|
+
ReleaseMessage,
|
|
18
|
+
AckMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__version__ = "7.0.0"
|
|
23
|
+
|
morp/compat.py
ADDED
morp/config.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import base64
|
|
3
|
+
|
|
4
|
+
import dsnparse
|
|
5
|
+
from datatypes import ReflectName
|
|
6
|
+
from datatypes import property as cachedproperty
|
|
7
|
+
from datatypes.config import Environ
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .compat import *
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Connection(object):
|
|
14
|
+
"""The base connection class, you will most likely always use DsnConnection
|
|
15
|
+
"""
|
|
16
|
+
name = ""
|
|
17
|
+
"""str, the name of this connection, handy when you have more than one used
|
|
18
|
+
interface (eg, nsq)"""
|
|
19
|
+
|
|
20
|
+
username = ""
|
|
21
|
+
"""str, the username (if needed)"""
|
|
22
|
+
|
|
23
|
+
password = ""
|
|
24
|
+
"""str, the password (if needed)"""
|
|
25
|
+
|
|
26
|
+
hosts = None
|
|
27
|
+
"""list, a list of (hostname, port) tuples"""
|
|
28
|
+
|
|
29
|
+
path = None
|
|
30
|
+
"""str, the path (if applicable)"""
|
|
31
|
+
|
|
32
|
+
interface_name = ""
|
|
33
|
+
"""str, full Interface class name -- the interface that will connect to the
|
|
34
|
+
messaging backend"""
|
|
35
|
+
|
|
36
|
+
options = None
|
|
37
|
+
"""dict, any other interface specific options you need"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def interface_class(self):
|
|
41
|
+
interface_class = ReflectName(self.interface_name).get_class()
|
|
42
|
+
return interface_class
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def interface(self):
|
|
46
|
+
interface_class = self.interface_class
|
|
47
|
+
return interface_class(self)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def serializer(self):
|
|
51
|
+
serializer = self.options.get("serializer", "pickle")
|
|
52
|
+
if serializer not in set(["pickle", "json"]):
|
|
53
|
+
raise ValueError(f"Unknown serializer {serializer}")
|
|
54
|
+
return serializer
|
|
55
|
+
|
|
56
|
+
@cachedproperty(cached="_key")
|
|
57
|
+
def key(self):
|
|
58
|
+
"""string -- an encryption key loaded from options['key'],
|
|
59
|
+
it must be 32 bytes long so this makes sure it is"""
|
|
60
|
+
key = self.options.get('key', "")
|
|
61
|
+
if key:
|
|
62
|
+
# Fernet key must be 32 url-safe base64-encoded bytes
|
|
63
|
+
bs = ByteString(ByteString(key).sha256())
|
|
64
|
+
key = base64.b64encode(bs[:32])
|
|
65
|
+
return key
|
|
66
|
+
|
|
67
|
+
def __init__(self, **kwargs):
|
|
68
|
+
"""set all the values by passing them into this constructor, any
|
|
69
|
+
unrecognized kwargs get put into .options
|
|
70
|
+
|
|
71
|
+
:example:
|
|
72
|
+
c = Connection(
|
|
73
|
+
interface_name="...",
|
|
74
|
+
hosts=[("host", port), ("host2", port2)],
|
|
75
|
+
some_random_thing="foo"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
print c.port # 5000
|
|
79
|
+
print c.options # {"some_random_thing": "foo"}
|
|
80
|
+
"""
|
|
81
|
+
self.options = kwargs.pop('options', {})
|
|
82
|
+
self.hosts = []
|
|
83
|
+
|
|
84
|
+
for key, val in kwargs.items():
|
|
85
|
+
if hasattr(self, key):
|
|
86
|
+
setattr(self, key, val)
|
|
87
|
+
|
|
88
|
+
else:
|
|
89
|
+
self.options[key] = val
|
|
90
|
+
|
|
91
|
+
# 1 hour to process message
|
|
92
|
+
self.options.setdefault('max_timeout', 3600)
|
|
93
|
+
|
|
94
|
+
# failure backoff multiplier
|
|
95
|
+
self.options.setdefault('backoff_multiplier', 5)
|
|
96
|
+
|
|
97
|
+
def get_netlocs(self, default_port):
|
|
98
|
+
return [
|
|
99
|
+
"{}:{}".format(
|
|
100
|
+
h[0],
|
|
101
|
+
default_port if h[1] is None else h[1]
|
|
102
|
+
) for h in self.hosts
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def get_option(self, key, default_val):
|
|
106
|
+
return getattr(self.options, key, default_val)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DsnConnection(Connection):
|
|
110
|
+
"""
|
|
111
|
+
Create a connection object from a dsn in the form
|
|
112
|
+
|
|
113
|
+
InterfaceName://username:password@host:port?opt1=val1#connection_name
|
|
114
|
+
|
|
115
|
+
example -- connect to amazon SQS
|
|
116
|
+
|
|
117
|
+
morp.interface.sqs.SQS://AWS_ID:AWS_KEY@
|
|
118
|
+
"""
|
|
119
|
+
def __init__(self, dsn):
|
|
120
|
+
self.dsn = dsn
|
|
121
|
+
d = self.parse(dsn)
|
|
122
|
+
super().__init__(**d)
|
|
123
|
+
|
|
124
|
+
def parse(self, dsn):
|
|
125
|
+
d = {'options': {}, 'hosts': []}
|
|
126
|
+
parser = dsnparse.parse(dsn)
|
|
127
|
+
p = parser.fields
|
|
128
|
+
d["dsn"] = parser.parser.dsn
|
|
129
|
+
|
|
130
|
+
# get the scheme, which is actually our interface_name
|
|
131
|
+
d['interface_name'] = self.normalize_scheme(p["scheme"])
|
|
132
|
+
|
|
133
|
+
dsn_hosts = []
|
|
134
|
+
if p["hostname"]:
|
|
135
|
+
d['hosts'].append((p["hostname"], p.get('port', None)))
|
|
136
|
+
|
|
137
|
+
d['options'] = p["query_params"] or {}
|
|
138
|
+
d['name'] = p["fragment"]
|
|
139
|
+
d['username'] = p["username"]
|
|
140
|
+
d['password'] = p["password"]
|
|
141
|
+
d["path"] = p["path"]
|
|
142
|
+
|
|
143
|
+
return d
|
|
144
|
+
|
|
145
|
+
def normalize_scheme(self, v):
|
|
146
|
+
ret = v
|
|
147
|
+
d = {
|
|
148
|
+
"morp.interface.sqs:SQS": set(["sqs"]),
|
|
149
|
+
"morp.interface.dropfile:Dropfile": set(["dropfile", "file"]),
|
|
150
|
+
"morp.interface.postgres:Postgres": set([
|
|
151
|
+
"postgres",
|
|
152
|
+
"postgresql",
|
|
153
|
+
"psql",
|
|
154
|
+
]),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
kv = v.lower()
|
|
158
|
+
for interface_name, vals in d.items():
|
|
159
|
+
if kv in vals:
|
|
160
|
+
ret = interface_name
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
return ret
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
environ = Environ("MORP_") # Load any MORP_* environment variables
|
|
167
|
+
|
|
168
|
+
environ.setdefault('DISABLED', False, type=bool)
|
|
169
|
+
"""If set to 1 then messages won't actually be sent"""
|
|
170
|
+
|
|
171
|
+
environ.setdefault('PREFIX', '')
|
|
172
|
+
"""This prefix will be used to create the queue, see Message.get_name()"""
|
|
173
|
+
|
morp/exception.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import unicode_literals, division, print_function, absolute_import
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Error(Exception):
|
|
6
|
+
"""error wrapper"""
|
|
7
|
+
def __init__(self, e, exc_info=None):
|
|
8
|
+
self.e = e
|
|
9
|
+
self.exc_info = exc_info
|
|
10
|
+
super(Error, self).__init__(str(e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InterfaceError(Error):
|
|
14
|
+
"""specifically for wrapping interface errors"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ReleaseMessage(InterfaceError):
|
|
19
|
+
"""Can be raised anytime in a recv() with block or target() method to get the
|
|
20
|
+
message to be released
|
|
21
|
+
|
|
22
|
+
:param delay_seconds: int, how long until message is released to be processed
|
|
23
|
+
again, the visibility timeout
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, delay_seconds=0):
|
|
26
|
+
super(ReleaseMessage, self).__init__("")
|
|
27
|
+
self.delay_seconds = delay_seconds
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AckMessage(InterfaceError):
|
|
31
|
+
"""Can be raised anytime in a recv() with block or target() method to get the
|
|
32
|
+
message to be acknowledged"""
|
|
33
|
+
def __init__(self):
|
|
34
|
+
super(AckMessage, self).__init__("")
|
|
35
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import dsnparse
|
|
4
|
+
|
|
5
|
+
from ..compat import *
|
|
6
|
+
from ..config import DsnConnection
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
interfaces = {}
|
|
10
|
+
"""holds all configured interfaces"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_interfaces():
|
|
14
|
+
global interfaces
|
|
15
|
+
if not interfaces:
|
|
16
|
+
configure_environ()
|
|
17
|
+
return interfaces
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_interface(connection_name=""):
|
|
21
|
+
"""get the configured interface that corresponds to connection_name"""
|
|
22
|
+
global interfaces
|
|
23
|
+
if not interfaces:
|
|
24
|
+
configure_environ()
|
|
25
|
+
return interfaces[connection_name]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_interface(interface, connection_name=""):
|
|
29
|
+
"""bind an .interface.Interface() instance to connection_name"""
|
|
30
|
+
global interfaces
|
|
31
|
+
interfaces[connection_name] = interface
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_environ(dsn_env_name='MORP_DSN', connection_class=DsnConnection):
|
|
35
|
+
"""Returns Connection instances found in the environment
|
|
36
|
+
|
|
37
|
+
configure interfaces based on environment variables
|
|
38
|
+
|
|
39
|
+
by default, when morp is imported, it will look for MORP_DSN, and MORP_DSN_N
|
|
40
|
+
(where N is 1 through infinity) in the environment, if it finds them, it
|
|
41
|
+
will assume they are dsn urls that morp understands and will configure
|
|
42
|
+
connections with them. If you don't want this behavior (ie, you want to
|
|
43
|
+
configure morp manually) then just make sure you don't have any environment
|
|
44
|
+
variables with matching names
|
|
45
|
+
|
|
46
|
+
The num checks (eg MORP_DSN_1, MORP_DSN_2) go in order, so you can't do
|
|
47
|
+
MORP_DSN_1, MORP_DSN_3, because it will fail on _2 and move on, so make sure
|
|
48
|
+
your num dsns are in order (eg, 1, 2, 3, ...)
|
|
49
|
+
|
|
50
|
+
:param dsn_env_name: str, the environment variable name to find the DSN,
|
|
51
|
+
this will aslo check for values of <dsn_env_name>_N where N is 1 -> N,
|
|
52
|
+
so you can configure multiple DSNs in the environment and this will pick
|
|
53
|
+
them all up
|
|
54
|
+
:param connection_class: Connection, the class that will receive the dsn
|
|
55
|
+
values
|
|
56
|
+
:returns: generator<connection_class>
|
|
57
|
+
"""
|
|
58
|
+
return dsnparse.parse_environs(dsn_env_name, parse_class=connection_class)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def configure_environ():
|
|
62
|
+
"""auto hook to configure the environment"""
|
|
63
|
+
for c in find_environ():
|
|
64
|
+
inter = c.interface
|
|
65
|
+
set_interface(c.interface, c.name)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def configure(dsn, connection_class=DsnConnection):
|
|
69
|
+
"""
|
|
70
|
+
configure an interface to be used to send messages to a backend
|
|
71
|
+
|
|
72
|
+
you use this function to configure an Interface using a dsn, then you can
|
|
73
|
+
get that interface using the get_interface() method
|
|
74
|
+
|
|
75
|
+
:param dsn: string, a properly formatted morp dsn, see DsnConnection for how
|
|
76
|
+
to format the dsn
|
|
77
|
+
"""
|
|
78
|
+
#global interfaces
|
|
79
|
+
c = dsnparse.parse(dsn, parse_class=connection_class)
|
|
80
|
+
set_interface(c.interface, c.name)
|
|
81
|
+
|