Custom endpoints¶
When you install Lime CRM, a central service is the Lime CRM Webserver (sometimes referred to as the appserver). The Lime CRM Webserver handles among other things:
- Security, such as certificates and authentication via logon via a web form or via an API key.
- Serving the Lime CRM REST API
- Serving the static assets and the API used by the web client
The Lime CRM REST API can be used for a multitude of integrations, but sometimes you'll want an API that is better suited for your particular integration. Uses cases for this might include:
- Having one endpoint that updates or creates more that one object and where you want to have those operations in one transaction
- Creating an API that better matches the business events for an organization than an API where you set individual properties on objects.
- Better control over how data is serialized back to the client.
With custom endpoints you can extend the API exposed by the Lime CRM Webserver with URLs that are better suited for your particular problem domain.
The Application¶
Lime CRM is at its core a multi-tenant server, meaning that several applications can be served by the same application server. That means that an endpoint cannot make assumptions or hard code information about, for instance, what database to connect to.
To relieve you from having to keep track of information about the
current database, its current structure etc., you can use the
application member of
LimeResource <lime-webserver:lime_webserver.webserver.LimeResource>
Adding a custom endpoint¶
To add a custom endpoint to your package, change directory to your package directory and run the command for generating the boiler plate for a custom endpoint:
This generates the following in cool-stuff/cool_stuff/endpoints/endpoint.py
:
import lime_webserver.webserver as webserver
import logging
import webargs.fields as fields
from webargs.flaskparser import use_args
from ..endpoints import api
logger = logging.getLogger(__name__)
class LimeobjectCounter(webserver.LimeResource):
"""Summarize your resource's functionality here"""
# This describes the schema for the payload when posting a new deal
# See https://webargs.readthedocs.io/en/latest/ for more info.
args = {
"limetype": fields.String(required=True),
}
@use_args(args)
def get(self, args):
"""Get the current number of objects of the given type in the system.
"""
limetype = self.application.limetypes.get_limetype(args['limetype'])
limeobjects = limetype.get_all()
return {
'message': (
'Hello, {}! There are {} objects of type {} available'
.format(
self.application.coworker.properties.firstname.value,
limeobjects.count,
limetype.name))}
api.add_resource(LimeobjectCounter, '/count/')
The generated module defines a resource, LimeobjectCounter, that is accessible
at the URL https://hostname/myapplication/cool-stuff/count/. The
URL we want to listen to is defined by the last line
api.add_resource(LimeobjectCounter, '/count/')
.
This particular resource only listens to HTTP GET requests as it
implements a method called get
. If we want to add support for other
HTTP methods, we need to add their corresponding methods, such as
post
, put
etc.
The get method requires the caller to supply a limetype argument in the
form of query strings. The args
dict at the beginning specifies that
we require the argument limetype
, and that it should be a string.
Together with the @use_args
decorator on the get method, we ensure
that the caller get a proper error message back if arguments are missing
or malformed.
Once we're satisfied that our get method has a proper limetype argument we proceed to get all objects of that type from the database via the resource's application property.
Note
We won't actually retrieve all objects from the database. The returned list is a lazy collection that starts retrieving objects as we iterate over it.
Finally, we return a dictionary with a greeting to the logged on coworker containing information about how many objects of the specified type is available in the database. The return value will automatically be formatted as a JSON response to the caller.
Adding automatically generated swagger documentation¶
To add automatically generated swagger documentation, make sure Marshmallow schemas are
defined. Note that everything below ---
in resource's docstring will be interpreted as
swagger documentation and it will support OpenAPI-Specification (v2 and v3) syntax.
import logging
import lime_webserver.webserver as webserver
from flask import make_response
from marshmallow import Schema, fields, validate
from webargs.flaskparser import use_args
from ..endpoints import api
class MyQuerySchema(Schema):
query_str_something = fields.String(required=True)
query_int = fields.Int()
path_int_schema = fields.Int(required=True, validate=validate.Range(min=5))
path_str_schema = fields.String(required=True, validate=validate.Length(max=10))
class MyPathSchema(Schema):
path_str = path_str_schema
path_int = path_int_schema
class MyBodySchema(Schema):
json_1 = fields.Int()
json_2 = fields.Bool()
json_3 = fields.Str()
class MyResponseSchema(Schema):
path = fields.Nested(MyPathSchema())
query = fields.Nested(MyQuerySchema())
json = fields.Nested(MyBodySchema())
class MyResource(webserver.LimeResource):
@use_args(MyBodySchema(), locations=("json", ))
@use_args(MyQuerySchema(), locations=("querystring",))
@use_args(
{"path_str": path_str_schema, "path_int": path_int_schema},
locations=("view_args", ),
as_kwargs=True
)
def post(self, json_data, query_data, path_str, path_int):
"""
---
tags:
- My Customs
parameters:
- in: path
schema: MyPathSchema
- in: query
schema: MyQuerySchema
- in: body
name: input
schema: MyBodySchema
responses:
201:
schema: MyResponseSchema
"""
resp = {
"path": {
"path_str": path_str,
"path_int": int(path_int),
},
"query": query_data,
"json": json_data
}
return make_response(resp, 201)
api.add_resource(MyResource, '/my-endpoint/<path_str>/<path_int>')
In endpoints/__init__.py
define and register API Spec and pass it to the Api:
import importlib
import logging
import pkgutil
import lime_endpoints.endpoints
from lime_apidocs import Spec, register_spec
from lime_endpoints.factory import Api
logger = logging.getLogger(__name__)
URL_PREFIX = '/solution-swagger-generation'
my_spec = Spec(
name="my spec",
version="1.0.0",
description="Here is nice description. It supports **markdown**",
url_prefix=URL_PREFIX,
consumes=["application/json"],
produces=["application/json"],
schemes=["http", "https"],
securityDefinitions={
"api_key": {"type": "apiKey", "name": "x-api-key", "in": "header"}
},
security=[{"api_key": []}],
)
register_spec(my_spec)
bp = lime_endpoints.endpoints.create_blueprint(
'solution_swagger_generation',
__name__,
URL_PREFIX)
def register_blueprint(app, config=None):
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
if module_name.endswith('_test'):
continue
module_fullname = '{}.{}'.format(__name__, module_name)
logger.info('Loading {}'.format(module_fullname))
importlib.import_module(module_fullname)
app.register_blueprint(bp)
return bp
api = Api(bp, spec=my_spec)
Finally, the automatically generated resource documentation will appear at the API Docs: