Welcome to Happyly’s documentation!¶
Introduction¶
Happyly is a scalable solution for systems which handle any kind of messages.
Happyly helps to abstract your business logic from messaging stuff, so that your code is maintainable and ensures separation of concerns.
Have you ever seen a codebase where serialization, message queue managing and business logic are mixed together like a spaghetti? I have. Imagine switching between Google Pub/Sub and Django REST Framework. Or Celery. This shouldn’t be a nightmare but it often is.
Here’s the approach of Happyly:
Write you business logic in universal Handlers, which don’t care at all how you serialize things or send them over network etc.
Describe your schemas using ORM/Framework-agnostic technology.
Plug-in any details of messaging protocol, serialization and networking. Change them with different drop-in replacements at any time.
Happyly can be used with Flask, Celery, Django, Kafka or whatever technology which can be used for messaging and also provides first-class support of Google Pub/Sub.
Use cases¶
Google Pub/Sub
Let’s be honest, the official Python client library is too low-level. You must serialize and deserialize things manually, as well as to
ack
andnack
messages.Usual way:
def callback(message): attributes = json.loads(message.data) try: result = process_things(attributes['ID']) encoded = json.dumps(result).encode('utf-8') PUBLISHER.publish(TOPIC, encoded) except NeedToRetry: _LOGGER.info('Not acknowledging, will retry later.') except Exception: _LOGGER.error('An error occured') message.ack() else: message.ack()
Happyly way:
def handle_my_stuff(message: dict): try: return process_things(message['ID']) except NeedToRetry as error: raise error from error except Exception: _LOGGER.error('An error occured')
handle_my_stuff
is now also usable with Celery or Flask. Or with yaml serialization. Or withmessage.attributes
instead ofmessage.data
. Without any change.You are going to change messaging technology later.
Let’s say you are prototyping your project with Flask and are planning to move to Celery for better fault tolerance then. Or to Google Pub/Sub. You just haven’t decided yet.
Easy! Here’s how Happyly can help.
Define your message schemas.
class MyInputSchema(happyly.Schema): request_id = marshmallow.fields.Str(required=True) class MyOutputSchema(happyly.Schema): request_id = marshmallow.fields.Str(required=True) result = marshmallow.fields.Str(required=True) error = marshmallow.fields.Str()
Define your handler
def handle_things(message: dict): try: req_id = message['request_id'] if req_id in ALLOWED: result = get_result_for_id(req_id) else: result = 'not allowed' return { 'request_id': req_id 'result': result } except Exception as error: return { 'request_id': message['request_id'] 'result': 'error', 'error': str(error) }
Plug it into Flask:
@app.route('/', methods=['POST']) def root(): executor = happyly.Executor( handler=handle_things, deserializer=DummyValidator(schema=MyInputSchema()), serializer=JsonifyForSchema(schema=MyOutputSchema()), ) request_data = request.get_json() return executor.run_for_result(request_data)
Painlessly switch to Celery when you need:
@celery.task('hello') def hello(message): result = happyly.Executor( handler=ProcessThings(), serializer=happyly.DummyValidator(schema=MyInputSchema()), deserializer=happyly.DummyValidator(schema=MyOutputSchema()), ).run_for_result( message ) return result
Or to Google Pub/Sub:
happyly.Listener( handler=ProcessThings(), deserializer=happyly.google_pubsub.JSONDeserializerWithRequestIdRequired( schema=MyInputSchema() ), serializer=happyly.google_pubsub.BinaryJSONSerializer( schema=MyOutputSchema() ), publisher=happyly.google_pubsub.GooglePubSubPublisher( topic='my_topic', project='my_project', ), ).start_listening()
5. Move to any other technology. Or swap serializer to another. Do whatever you need while your handler and schemas remain absolutely the same.
Installation¶
Happyly is hosted on PyPI, so you can use:
pip install happyly
There are extra dependencies for some components. If you want to use Happyly’s components for Flask, install it like this:
pip install happyly[flask]
There is also an extra dependency which enables cached components via Redis. If you need it, install Happyly like this:
pip install happyly[redis]
Concepts¶
Handler¶
Handler is the main concept of all Happyly library. Basically a handler is a callable which implements business logic, and nothing else:
No serialization/deserialiation here
No sending stuff over the network
No message queues’ related stuff
Let the handler do its job!
To create a handler you can simply define a function which takes a dict
as an input
and returns a dict
:
def handle_my_stuff(message: dict):
try
db.update(message['user'], message['status'])
return {
'request_id': message['request_id'],
'action': 'updated',
}
except Exception:
return {
'action': 'failed'
}
Done! This handler can be plugged into your application: whether it uses Flask or Celery or whatever.
Note that you are allowed to return nothing if you don’t actually need a result from your handler. This handler is also valid:
def handle_another_stuff(message: dict):
try
neural_net.start_job(message['id'])
_LOGGER.info('Job created')
except Exception:
_LOGGER.warning('Failed to create a job')
If you prefer class-based approach, Happyly can satisfy you too.
Subclass happyly.Handler()
and implement the following methods:
class MyHandler(happyly.Handler):
def handle(message: dict)
db.update(message['user'], message['status'])
return {
'request_id': message['request_id'],
'action': 'updated',
}
def on_handling_failed(message: dict, error)
return {
'action': 'failed'
}
Instance of MyHandler
is equivalent to handle_my_stuff
Executor¶
To plug a handler into your application you will need happyly.Executor()
(or one of its subclasses).
Advanced¶
API Reference¶
|
|
|
|