morp 5.2.2__tar.gz → 7.0.0__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.
morp-7.0.0/PKG-INFO ADDED
@@ -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&param2=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
+ ```
morp-7.0.0/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # Morp
2
+
3
+ Simple message processing without really thinking about it. Morp can use dropfiles (simple text files), Postgres, and [Amazon SQS](http://aws.amazon.com/sqs/).
4
+
5
+
6
+ ## Installation
7
+
8
+ Use pip to install the latest stable version:
9
+
10
+ pip install morp
11
+
12
+ 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:
13
+
14
+ pip install morp[sqs]
15
+ pip install morp[postgres]
16
+
17
+ To install the development version:
18
+
19
+ pip install -U "git+https://github.com/Jaymon/morp#egg=morp"
20
+
21
+
22
+ ## 1 Minute Getting Started
23
+
24
+ Send and receive a `Foo` message.
25
+
26
+ First, let's set our environment variable to use dropfiles (local files suitable for development and prototyping) interface:
27
+
28
+ export MORP_DSN=dropfile:${TMPDIR}
29
+
30
+ Then, let's create three files in our working directory:
31
+
32
+ * `tasks.py` - We'll define our `Message` classes here.
33
+ * `send.py` - We'll send messages from this script.
34
+ * `recv.py` - We'll receive messages from this script.
35
+
36
+
37
+ Let's create our `Message` class in `tasks.py`:
38
+
39
+ ```python
40
+ # tasks.py
41
+ from morp import Message
42
+
43
+ class Foo(Message):
44
+ some_field: int
45
+ some_other_field: str
46
+
47
+ def handle(self):
48
+ # this will be run when a Foo message is consumed
49
+ print(self.fields)
50
+ ```
51
+
52
+ Now, let's flesh out our `recv.py` file:
53
+
54
+ ```python
55
+ # recv.py
56
+
57
+ import asyncio
58
+
59
+ # import our Foo message class from our tasks.py file
60
+ from tasks import Foo
61
+
62
+ # Foo's `process` method will call `Foo.handle` for each Foo instance received
63
+ asyncio.run(Foo.process())
64
+ ```
65
+
66
+ And start it up:
67
+
68
+ ```
69
+ $ python recv.py
70
+ ```
71
+
72
+
73
+ Finally, let's send some messages by fleshing out `send.py`:
74
+
75
+ ```python
76
+ # send.py
77
+
78
+ import asyncio
79
+
80
+ from tasks import Foo
81
+
82
+ async def send_messages():
83
+ # create a message and send it manually
84
+ f = Foo()
85
+ f.some_field = 1
86
+ f.some_other_field = "one"
87
+ f.ignored_field = True
88
+ await f.send()
89
+
90
+ # quickly send a message
91
+ await Foo.create(
92
+ some_field=2,
93
+ some_other_field="two",
94
+ )
95
+
96
+ asyncio.run(send_messages())
97
+ ```
98
+
99
+ And running it in a separate shell from the shell running our `recv.py` script (it should send two messages):
100
+
101
+ ```
102
+ $ python send.py
103
+ ```
104
+
105
+ That's it! Our running `recv.py` script should've received the messages we sent when we ran our `send.py` script.
106
+
107
+
108
+ ## DSN
109
+
110
+ You configure your connection using a dsn in the form:
111
+
112
+ InterfaceName://username:password@host:port/path?param1=value1&param2=value2
113
+
114
+ So, to connect to [Amazon SQS](http://aws.amazon.com/sqs/), you would do:
115
+
116
+ sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@
117
+
118
+ You can also override some default values like `region` and `read_lock`:
119
+
120
+ sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@?region=${AWS_DEFAULT_REGION}&read_lock=120
121
+
122
+
123
+ ### Serializers
124
+
125
+ * `pickle` (default)
126
+ * `json`
127
+
128
+ ```
129
+ MORP_DSN="sqs://x:x@?serializer=json"
130
+ ```
131
+
132
+
133
+ ## Encryption
134
+
135
+ You might need to install some dependencies:
136
+
137
+ ```
138
+ pip install morp[encryption]
139
+ ```
140
+
141
+ 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.
142
+
143
+ Let's just modify our DSN to pass in our key:
144
+
145
+ sqs://${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}@?key=jy4XWRuEsrH98RD2VeLG62uVLCPWpdUh
146
+
147
+ 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.
148
+
149
+
150
+ ## Environment configuration
151
+
152
+ ### MORP_DISABLED
153
+
154
+ 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.
155
+
156
+ MORP_DISABLED = 1 # queue is off
157
+ MORP_DISABLED = 0 # queue is on
158
+
159
+
160
+ ### MORP_PREFIX
161
+
162
+ 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.
163
+
164
+
165
+ ### MORP_DSN
166
+
167
+ Set this environment variable with your connection DSN so Morp can automatically configure itself when the interface is first requested.
168
+
169
+
170
+ ## FAQ
171
+
172
+ ### I would like to have multiple queues
173
+
174
+ 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:
175
+
176
+ ```python
177
+ from morp import Message
178
+
179
+ class childMsg(Message):
180
+ _name = "custom-queue-name"
181
+ ```
@@ -19,5 +19,5 @@ from .exception import (
19
19
  )
20
20
 
21
21
 
22
- __version__ = '5.2.2'
22
+ __version__ = "7.0.0"
23
23
 
@@ -1,11 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
- from __future__ import unicode_literals, division, print_function, absolute_import
3
- import hashlib
4
- import os
5
2
  import base64
6
3
 
7
4
  import dsnparse
8
- from datatypes import ReflectClass
5
+ from datatypes import ReflectName
9
6
  from datatypes import property as cachedproperty
10
7
  from datatypes.config import Environ
11
8
 
@@ -14,9 +11,11 @@ from .compat import *
14
11
 
15
12
 
16
13
  class Connection(object):
17
- """The base connection class, you will most likely always use DsnConnection"""
14
+ """The base connection class, you will most likely always use DsnConnection
15
+ """
18
16
  name = ""
19
- """str, the name of this connection, handy when you have more than one used interface (eg, nsq)"""
17
+ """str, the name of this connection, handy when you have more than one used
18
+ interface (eg, nsq)"""
20
19
 
21
20
  username = ""
22
21
  """str, the username (if needed)"""
@@ -31,14 +30,15 @@ class Connection(object):
31
30
  """str, the path (if applicable)"""
32
31
 
33
32
  interface_name = ""
34
- """str, full Interface class name -- the interface that will connect to the messaging backend"""
33
+ """str, full Interface class name -- the interface that will connect to the
34
+ messaging backend"""
35
35
 
36
36
  options = None
37
37
  """dict, any other interface specific options you need"""
38
38
 
39
39
  @property
40
40
  def interface_class(self):
41
- interface_class = ReflectClass.get_class(self.interface_name)
41
+ interface_class = ReflectName(self.interface_name).get_class()
42
42
  return interface_class
43
43
 
44
44
  @property
@@ -65,10 +65,10 @@ class Connection(object):
65
65
  return key
66
66
 
67
67
  def __init__(self, **kwargs):
68
- """
69
- set all the values by passing them into this constructor, any unrecognized kwargs get put into .options
68
+ """set all the values by passing them into this constructor, any
69
+ unrecognized kwargs get put into .options
70
70
 
71
- :Example:
71
+ :example:
72
72
  c = Connection(
73
73
  interface_name="...",
74
74
  hosts=[("host", port), ("host2", port2)],
@@ -84,14 +84,23 @@ class Connection(object):
84
84
  for key, val in kwargs.items():
85
85
  if hasattr(self, key):
86
86
  setattr(self, key, val)
87
+
87
88
  else:
88
89
  self.options[key] = val
89
90
 
90
- self.options.setdefault('max_timeout', 3600) # 1 hour to process message
91
- self.options.setdefault('backoff_multiplier', 5) # failure backoff multiplier
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)
92
96
 
93
97
  def get_netlocs(self, default_port):
94
- return ["{}:{}".format(h[0], default_port if h[1] is None else h[1]) for h in self.hosts]
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
+ ]
95
104
 
96
105
  def get_option(self, key, default_val):
97
106
  return getattr(self.options, key, default_val)
@@ -101,7 +110,7 @@ class DsnConnection(Connection):
101
110
  """
102
111
  Create a connection object from a dsn in the form
103
112
 
104
- InterfaceName://username:password@host:port?opt1=val1&opt2=val2#connection_name
113
+ InterfaceName://username:password@host:port?opt1=val1#connection_name
105
114
 
106
115
  example -- connect to amazon SQS
107
116
 
@@ -116,7 +125,7 @@ class DsnConnection(Connection):
116
125
  d = {'options': {}, 'hosts': []}
117
126
  parser = dsnparse.parse(dsn)
118
127
  p = parser.fields
119
- p["dsn"] = parser.parser.dsn
128
+ d["dsn"] = parser.parser.dsn
120
129
 
121
130
  # get the scheme, which is actually our interface_name
122
131
  d['interface_name'] = self.normalize_scheme(p["scheme"])
@@ -137,7 +146,12 @@ class DsnConnection(Connection):
137
146
  ret = v
138
147
  d = {
139
148
  "morp.interface.sqs:SQS": set(["sqs"]),
140
- "morp.interface.dropfile:Dropfile": set(["dropfile"]),
149
+ "morp.interface.dropfile:Dropfile": set(["dropfile", "file"]),
150
+ "morp.interface.postgres:Postgres": set([
151
+ "postgres",
152
+ "postgresql",
153
+ "psql",
154
+ ]),
141
155
  }
142
156
 
143
157
  kv = v.lower()
@@ -1,20 +1,9 @@
1
1
  # -*- coding: utf-8 -*-
2
- from __future__ import unicode_literals, division, print_function, absolute_import
3
- import logging
4
- import sys
5
- from contextlib import contextmanager
6
- import base64
7
- import datetime
8
-
9
- from cryptography.fernet import Fernet
2
+
10
3
  import dsnparse
11
4
 
12
5
  from ..compat import *
13
6
  from ..config import DsnConnection
14
- from ..exception import InterfaceError
15
-
16
-
17
- logger = logging.getLogger(__name__)
18
7
 
19
8
 
20
9
  interfaces = {}
@@ -47,20 +36,23 @@ def find_environ(dsn_env_name='MORP_DSN', connection_class=DsnConnection):
47
36
 
48
37
  configure interfaces based on environment variables
49
38
 
50
- by default, when morp is imported, it will look for MORP_DSN, and MORP_DSN_N (where
51
- N is 1 through infinity) in the environment, if it finds them, it will assume they
52
- are dsn urls that morp understands and will configure connections with them. If you
53
- don't want this behavior (ie, you want to configure morp manually) then just make sure
54
- you don't have any environment variables with matching names
55
-
56
- The num checks (eg MORP_DSN_1, MORP_DSN_2) go in order, so you can't do MORP_DSN_1, MORP_DSN_3,
57
- because it will fail on _2 and move on, so make sure your num dsns are in order (eg, 1, 2, 3, ...)
58
-
59
- :param dsn_env_name: str, the environment variable name to find the DSN, this
60
- will aslo check for values of <dsn_env_name>_N where N is 1 -> N, so you
61
- can configure multiple DSNs in the environment and this will pick them all
62
- up
63
- :param connection_class: Connection, the class that will receive the dsn values
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
64
56
  :returns: generator<connection_class>
65
57
  """
66
58
  return dsnparse.parse_environs(dsn_env_name, parse_class=connection_class)
@@ -77,10 +69,11 @@ def configure(dsn, connection_class=DsnConnection):
77
69
  """
78
70
  configure an interface to be used to send messages to a backend
79
71
 
80
- you use this function to configure an Interface using a dsn, then you can get
81
- that interface using the get_interface() method
72
+ you use this function to configure an Interface using a dsn, then you can
73
+ get that interface using the get_interface() method
82
74
 
83
- :param dsn: string, a properly formatted morp dsn, see DsnConnection for how to format the dsn
75
+ :param dsn: string, a properly formatted morp dsn, see DsnConnection for how
76
+ to format the dsn
84
77
  """
85
78
  #global interfaces
86
79
  c = dsnparse.parse(dsn, parse_class=connection_class)