General description
Independently of lambda type there are several general things whose applicable for every lambda.
Luna lambda tools
It is required the luna-lambda-tools package which is available at VL pypi for development. This package is available as luna_lambda_tools library and will be used in all examples below.
To install luna-lambda-tools locally, execute:
pip install --trusted-host pypi.visionlabs.ru --extra-index-url http://pypi.visionlabs.ru/root/public/+simple luna-lambda-tools
The luna-lambda-tools package decided in two parts - public and private: private part is not intended for user and does not guarantee backward compatibility, public part is intended for user and guarantees backward compatibility.
User context
It is possible to make the lambda do some stuff after startup and before shutdown declaring UserCtx class with asynchronous onStart and/or onShutdown functions.
For example, it is possible to initialize and close connection to database:
from luna_lambda_tools import logger
def setupDB():
...
def closeDBConnections():
...
class UserCtx:
async def onStart(self):
logger.info("start lambda")
setupDB()
async def onShutdown(self):
logger.info("shutdown lambda")
closeDBConnections()
It is possible to use initialize UserCtx at application start, use during request processing and close at application shutdown:
from luna_lambda_tools import StandaloneLambdaRequest
class UserCtx:
def __init__(self):
self.value = 0
async def onStart(self):
self.value = 1
async def onShutdown(self):
self.value = 0
def getValue(self):
return self.value
async def main(request: StandaloneLambdaRequest) -> dict:
return {"result": request.userCtx.getValue()}
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
This example demonstrates making requests to external server using for session for all requests:
import aiohttp
from luna_lambda_tools import StandaloneLambdaRequest, logger
class UserCtx:
def __init__(self):
self.session = None
async def onStart(self):
logger.info("start lambda")
self.session = aiohttp.ClientSession()
async def onShutdown(self):
logger.info("shutdown lambda")
await self.session.close()
async def main(request: StandaloneLambdaRequest) -> dict:
url = "https://docs.visionlabs.ai/luna"
async with request.userCtx.session.get(url) as response:
statusCode = response.status
return {"status": statusCode}
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
Logger
It is possible to use logger from luna-lambda-tools anywhere in lambda.
from luna_lambda_tools import StandaloneLambdaRequest, logger
async def main(request: StandaloneLambdaRequest) -> dict:
logger.info("Hello, world!")
return {"result": "empty"}
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
Lambda modules structure
It is possible to add and use more than one python module. Here is an example of file structure and python source code which is required for proper lambda work.
├──lambda_main.py
├──path
│ ├──fileinpath1.py
│ └──fileinpath2.py
└──file1.py
from file1 import foo_from_file1
from luna_lambda_tools import StandaloneLambdaRequest
from path import fileinpath1
from path.fileinpath2 import foo_from_fileinpath2
async def main(request: StandaloneLambdaRequest) -> dict:
return {
"from_file_1": foo_from_file1(),
"variable_from_nested": fileinpath1.get_variable_from_another_file(),
"from_file_in_path_1": fileinpath1.foo_from_fileinpath1(),
"from_file_in_path_2": foo_from_fileinpath2(),
}
def foo_from_file1():
return "foo_from_file1"
from .fileinpath2 import variable
def foo_from_fileinpath1():
return "foo_from_fileinpath1"
def get_variable_from_another_file():
return variable
variable = 1
def foo_from_fileinpath2():
return "foo_from_fileinpath2"
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
User configuration file
It is possible to add a configuration file for lambda in YAML format. The file must be named lambda_config.yml and placed in the root of the zip archive.
├──lambda_main.py
└──lambda_config.yml
You can import this configuration to your lambda code and use as python dictionary object.
name: "My Lambda"
version: 5
database:
type: "postgresql"
host: "10.8.0.1"
port: 5432
name: "my_lambda_db"
user: "db_user"
password: "db_password"
logging:
level: "info"
format: "text"
features:
feature_1: true
feature_2: false
users:
- name: "admin"
email: "admin@example.com"
- name: "user"
email: "user@example.com"
from luna_lambda_tools import StandaloneLambdaRequest, lambdaConfig
async def main(request: StandaloneLambdaRequest) -> dict:
result = {
"title": lambdaConfig["name"],
"version": lambdaConfig["version"],
"feature1_enabled": lambdaConfig["features"]["feature_1"],
"username": lambdaConfig["users"][0]["name"],
}
return result
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
Lambda exceptions
It is possible to separate exceptions into two types: expected and unexpected. Expected exceptions must be inherited from UserException. For example:
from luna_lambda_tools import StandaloneLambdaRequest, UserException
class ImageCountException(UserException):
statusCode = 400
errorText = "expected two images in request"
async def main(request: StandaloneLambdaRequest) -> dict:
if len(request.json["images"]) != 2:
raise ImageCountException
return {"image_count": len(request.json["images"])}
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
data = {"images": ["image_one"]}
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId, body=data)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
In the case presented above for any request with images quantity other than two, it will raise ImageCountException and then, luna-handlers will process exception and return a response to the user with 400 status code and expected two images in request message in detail.
Unexpected exceptions will proceed in another way. If any exception occurs, luna-handlers will return a response to the user with 500 status code and exception text in detail. For example:
from luna_lambda_tools import StandaloneLambdaRequest
async def main(request: StandaloneLambdaRequest) -> dict:
abc = 1/0
...
will cause response with division by zero as exception detail.
Luna services clients
The lambda can use luna-services for different operations.
The clients from luna-lambda-tools represent wrap for VL luna3 library (also available at VL pypi). As well as luna-lambda-tools, luna3 has a public part that is intended for user and guarantee backward compatibility.
Example of clients usage for standalone/handlers lambdas (for clients usage in tasks lambdas see tasks lambda examples):
from luna_lambda_tools import StandaloneLambdaRequest
async def main(request: StandaloneLambdaRequest) -> dict:
versions = {
"faces_address": request.clients.faces.getAddress(),
}
if request.eventsEnabled:
versions["events_address"] = request.clients.events.getAddress()
return versions
from luna3.luna_lambda.luna_lambda import LambdaApi
SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start
SERVER_API_VERSION = 1
lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION)
lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start
def makeRequest():
reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path="main", accountId=accountId)
return reply
if __name__ == "__main__":
response = makeRequest()
print(response.json)
It also possible to check whether luna service enabled or disabled using several properties:
from luna_lambda_tools import StandaloneLambdaRequest
async def main(request: StandaloneLambdaRequest) -> dict:
request.logger.info(f"events enabled: {request.eventsEnabled}")
request.logger.info(f"sender enabled: {request.senderEnabled}")
request.logger.info(f"handlers enabled: {request.handlersEnabled}")
...
It can be useful if lambda can optional use one of this service, for example, save event if Luna-Events is enabled:
from luna_lambda_tools import StandaloneLambdaRequest
async def main(request: StandaloneLambdaRequest) -> dict:
if request.eventsEnabled:
request.clients.events.saveEvents(events=[...])
return {"status": "success"}
Additional routes
Each lambda type can be extended with additional routes with custom functions. For example:
standalone lambda with additional routes. Additional routes for handlers lambda can be added the same way.
lambda_main.pyfrom luna_lambda_tools import LambdaUserHandler, StandaloneLambdaRequest UUID_REGEXP_STR = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}" PARAMETERS = [] class ParameterHandler(LambdaUserHandler): """Parameter handler""" route: str = f"/parameters/<parameter:{UUID_REGEXP_STR}>" async def post(self, parameter): PARAMETERS.append(parameter) return self.sendResponse(201, json={"response": parameter}) async def head(self, parameter): if parameter not in PARAMETERS: return self.sendResponse(404, body="Parameter not found", headers={"Content-Type": "text/plain"}) return self.sendResponse(204) class ParametersHandler(LambdaUserHandler): """Parameters handler""" route: str = "/parameters" async def get(self): return self.sendResponse(200, body="; ".join(PARAMETERS), headers={"Content-Type": "text/plain"}) ROUTES_TO_ADD = [ParameterHandler, ParametersHandler] async def main(request: StandaloneLambdaRequest) -> dict: """Main route""" return {"result": "main route response"}
request examplefrom uuid import uuid4 from luna3.luna_lambda.luna_lambda import LambdaApi SERVER_ORIGIN = "http://lambda_address:lambda_port" # Replace by your values before start SERVER_API_VERSION = 1 lambdaApi = LambdaApi(origin=SERVER_ORIGIN, api=SERVER_API_VERSION) lambdaId, accountId = "your_lambda_id", "your_account_id" # Replace by your values before start def makeRequest(): reply = lambdaApi.proxyLambdaPost(lambdaId=lambdaId, path=f"parameters/{str(uuid4())}", accountId=accountId) return reply if __name__ == "__main__": response = makeRequest() print(response.json)
tasks lambda with additional routes.
lambda_main.pyimport asyncio from collections import defaultdict from luna_lambda_tools import LambdaUserHandler from luna_lambda_tools.public.tasks import BaseLambdaTask SUBTASKS_DONE = defaultdict(lambda: 0) class DoneSubtasksHandler(LambdaUserHandler): """Subtasks handler""" route: str = r"/subtasks_done/<taskId:\d+>" async def get(self, taskId): """Get subtasks done""" if (count := SUBTASKS_DONE.get(int(taskId), None)) is None: return self.sendResponse(404, json={"error": f"Task {taskId} not found"}) return self.sendResponse(200, json={"sub_tasks_done": count}) ROUTES_TO_ADD = [DoneSubtasksHandler] class LambdaTask(BaseLambdaTask): """Lambda task""" async def splitTasksContent(self, content: dict) -> list[dict]: """Split task content to sub task contents""" return [{"content": data} for data in content["data"]] async def executeSubtask(self, subtaskContent: dict) -> dict | list: """Execute current sub task processing""" for _ in range(subtaskContent["content"]["count"]): await asyncio.sleep(subtaskContent["content"]["delay"]) SUBTASKS_DONE[self._data.taskId] += 1 return {"result": "some task result"}
Each handler that needs to be added to lambda must be part of the constant array ROUTES_TO_ADD within lambda_main.py file, otherwise handler will be ignored.
Note that `ROUTES_TO_ADD` assignment must follow this format: ROUTES_TO_ADD = [AdditionalHandler, ...]
. Assignments from functions, other variables, etc. will cause validation error.
- There are strict requirements for the format of custom handlers:
Handler must be declared within lambda_main.py
Handler must be inherited from LambdaUserHandler
Handler must have route attribute assignment within class body. Note that `route` must be assigned to a constant or formatted string, not to a function result or other variable
Handler must implement at least one of the async methods: post, get, put, patch, head, options, delete
If route includes path parameters, this parameters must be included into method signature with the same name. See routing format description here.
Custom route must not match default routes such as /healthcheck, /main, /docs/spec, /config
Note that `route` and http `post`, `get`, `put`, `patch`, `head`, `options`, `delete` methods must be declared within one final class (not in parent classes)
Breaking one of this requirement will cause lambda validation error
Some examples of handlers declaration.
route without parameters.
lambda_main.pyclass AdditionalHandler(LambdaUserHandler): route: str = "/new_route" async def post(self): return self.sendResponse(201, json={"response": parameter}) ROUTES_TO_ADD = [AdditionalHandler]
route with two parameters.
lambda_main.pyclass AdditionalHandler(LambdaUserHandler): route: str = r"/new_route/<parameterOne:.*>/<parameterTwo:.*>" async def post(self, parameterOne, parameterTwo): return self.sendResponse(201, json={"response": parameter}) ROUTES_TO_ADD = [AdditionalHandler]
Handler with inheritance.
lambda_main.pyclass BaseAdditionalHandler(LambdaUserHandler): def getResponse(self, parameter): return {"response": parameter} class AdditionalHandler(BaseAdditionalHandler): route: str = r"/new_route/<parameter:.*>" async def post(self, parameter): return self.sendResponse(201, json=self.getResponse(parameter)) ROUTES_TO_ADD = [AdditionalHandler]