Services
A nanopie service consists of a number of endpoints, each equipped with one or more handlers chained together for authentication, serialization/deserialization, logging, tracing, and more.
Under the hood a nanopie service integrates with a Python web framework as transport for the flow of data. Loosely speaking, you can think of nanopie services as a way of helping you configure the web framework of your choice. If you want to, you may still write your application logic following the principles of your preferred frameworks.
nanopie plans to provide a variety of services of different types (HTTP, event-driven, etc.) with support for different frameworks. At this early stage of development, however, nanopie supports only HTTP (RESTful) microservices/API backend with the Flask micro-framework as transport.
Using HTTP services
You may use the following framework as transport with a nanopie HTTP service at this moment:
| Framework | Service Class | Description |
|---|---|---|
| Flask | FlaskService |
An HTTP service with the Flask micro-framework as transport. |
Creating the service
To create a nanopie HTTP service, build an application using your preferred framework and wrap it with the corresponding nanopie service class.
You must specify what data interchange format you would like to use for serialization and deserialization when you create the service by setting up a serialization helper. By default nanopie HTTP services use the JSON format; if you would like to change this setting and use a different serialization helper, see Serialization for instructions. In addition, if you have an authentication, logging, or tracing handler that you would like to apply to all endpoints in a service, you should specify them when you create the service as well.
from flask import Flask
from nanopie import FlaskService
app = Flask(__name__)
svc = FlaskService(app=app)
Service class arguments
| Argument | Required | Type and Default Value | Description |
|---|---|---|---|
app |
Yes | N/A | The application class from a transport. |
authn_handler |
No | AuthenticationHandler, None |
The authentication handler that the service should apply to all endpoints. See Authentication for more information. |
logging_handler |
No | LoggingHandler, None |
The logging handler that the service should apply to all endpoints. See Logging for more information. |
tracing_handler |
No | TracingHandler, None |
The tracing handler that the service should apply to all endpoints. See Tracing for more information. |
serialization_helper |
No | SerializationHelper, None |
The serialization helper that the service should use. See Serialization for more information. |
max_content_length |
No | 6000 |
The maximum length of requests. |
Adding endpoints
A nanopie HTTP service provides a number of decorators for you to create common HTTP RESTful endpoints:
| Decorator | RESTful Endpoint | Description |
|---|---|---|
create |
CREATE |
A CREATE endpoint using the HTTP POST verb, usually used for create new resources. |
get |
GET |
A GET (READ) endpoint using the HTTP GET verb, usually used for retrieving resources. |
update |
UPDATE |
An UPDATE endpoint using the HTTP PATCH verb, usually used for updating resources. |
delete |
DELETE |
A DELETE endpoint using the HTTP DELETE verb, usually used for deleting resources. |
list |
- | An endpoint using the HTTP GET verb, usually used for listing resources. |
custom |
- | An endpoint using a custom verb (mapped to one of the HTTP verbs), usually used for custom operations on resources. |
# svc is the nanopie service created in the previous step
# This will create an endpoint at `/users` that accepts HTTP GET requests
# and return a list of users.
@svc.list(name="list_users",
rule="/users")
def list_users():
do_something()
# This will create a custom endpoint at `/users/USER-ID:verify` with the `GET`
# HTTP verb for verifying users.
@svc.custom(name="verify_user",
rule="/users/<int:user_id>",
verb="GET",
method="verify")
def verify_user(user_id):
do_something()
If you prefer not using the decorators, nanopie services also provide the
following methods for creating endpoints; these methods take the same
arguments that decorators accept, with an addition, the func keyword argument.
add_create_endpointadd_get_endpointadd_update_endpointadd_delete_endpointadd_list_endpointadd_custom_endpoint
def list_users():
do_something()
svc.add_list_endpoint(name="list_users",
rule="/users",
func=list_users)
Arguments for create, get, update, delete, and list decorators
| Argument | Required | Type and Default Value | Description |
|---|---|---|---|
name |
Yes | str, N/A |
The name of the endpoint. |
rule |
Yes | str, None |
The URL rule associated with the endpoint. See Rules for more information. |
data_cls |
Yes for create and update decorators |
ModelMetaCls, N/A for create and update decorators, None for others |
The data model for the request payload. See Data models for more information. |
headers_cls |
No | ModelMetaCls, None |
The data model for the headers of requests. See Data models for more information. |
query_args_cls |
No | ModelMetaCls, None |
The data model for the query arguments of requests. See Data models for more information. |
authn_handler |
No | AuthenticationHandler, None |
The authentication handler applied to this endpoint. See Endpoint specific authentication, logging, and tracing handlers for more information. |
logging_handler |
No | LoggingHandler, None |
The logging handler applied to this endpoint. See Endpoint specific authentication, logging, and tracing handlers for more information. |
tracing_handler |
No | OpenTelemetryTracingHandler, None |
The tracing handler applied to this endpoint. See Endpoint specific authentication, logging, and tracing handlers for more information. |
extras |
No | Dict, None |
User-supplied additional information about the endpoint. See Extras for more information. |
Arguments for custom decorators
custom decorators support all the arguments of create, get, update,
delete, and list decorations, with the following additional arguments:
| Additional Argument | Required | Type and Default Value | Description |
|---|---|---|---|
verb |
Yes | str, N/A |
The HTTP verb associated with the endpoint. It should be one of GET, POST, PUT, PATCH, DELETE, UPDATE, TRACE, OPTIONS, and CONNECT. |
method |
Yes | str, N/A |
The custom method, such as verify, associated with the endpoint. |
Rules
Each endpoint is associated with a URL rule, which dictates the path of the
endpoint, such as /users. If the path includes a variable part (path
parameter), specify it with the syntax <TYPE:NAME>. For example, a
GET endpoint for a user management API may live at the path /users/USER-ID,
where the USED-ID is the integer ID of a user; and its URL rule should be
expressed as /users/<int:user_id>. nanopie will send the name of the variable
part and its value parsed from the URL to the decorated method as keyword
arguments.
The following types are available:
| Type | Description |
|---|---|
string |
A string typed variable (without slashes). |
int |
An integer typed variable. |
float |
A float typed variable. |
path |
A string typed variable with slashes. |
any |
A variable of any type. |
uuid |
A UUID formatted string. |
The code snippet below creates a GET endpoint for getting a user; if you
run the service and access /users/1, the get_user method will receive a
keyword argument user_id=1.
@svc.get(name="get_user",
rule="/users/<int:user_id>")
def get_user(user_id):
do_something()
Data models
Aside from the path parameters specified in the rules, HTTP microservices and API backends also accept parameters in the headers and query strings of the HTTP request, with the resources themselves being transferred in the payload. nanopie allows developers to model the parameters and the resources with nanopie data models, and you can add these models to your endpoints via the decorators so that nanopie services can deserialize them from the raw data in the HTTP requests into instances of your data models automatically.
The code snippet below creates a CREATE endpoint for creating a user; it
accepts HTTP requests with serialized User objects
(e,g, { "name": "Albert Wesker" }, if using JSON as the data interchange
format) as payload. nanopie will deserialize the payload automatically into
an instance of the User data model; see Requests for
instructions on how to access it.
class User(Model):
name = StringField()
@svc.create(name="create_user",
rule="/users",
data_cls=User)
def create_user():
do_something()
Endpoint specific authentication, logging, and tracing handlers
You can add individual authentication, logging, and tracing handlers to an endpoint. This will override the service-wide settings (if any).
Note
See Authentication, Logging, and Tracing for instructions on using authentication, logging, and tracing handlers.
The code snippet below creates two endpoints, get_user and create_user,
with tracing enabled only on the create_user endpoint.
@svc.get(name="get_user",
rule="/users/<int:user_id>")
def get_user(user_id):
do_something()
tracing_handler = OpenTelemetryTracingHandler()
@svc.create(name="create_user",
rule="/users",
data_cls=User,
tracing_handler=tracing_handler)
def create_user():
do_something()
Writing the application logic
As stated in the beginning of this document, in some way what nanopie does
is merely helping you configure your preferred framework for running
microservices and API backends. Therefore, when you use decorators
from nanopie services to decorate a method and write your application
logic for a specific endpoint in the method, most designs and patterns
from your preferred framework will continue to work. For example, if you are
using a Flask application as transport, you can still access the raw data in
the request using the Flask.request global proxy, or return an HTTP
response with the make_response method.
from flask import Flask, request, make_response
from nanopie import FlaskService
app = Flask(__name__)
svc = FlaskService(app=app)
@svc.get(name="get_user",
rule="/users/<int:user_id>")
def get_user(user_id):
# Access the raw headers using the Flask request global proxy
raw_headers = request.headers
# Return a 500 response with the make_response method from Flask
return make_response("Not implemented yet.", 500)
Still, nanopie offers its own ways to manipulate the flow of requests and responses, as specified below.
Requests
nanopie provides two global proxies, request and parsed_request, which
you can use to access information in incoming HTTP requests. As global
proxies, values in these proxies are set when the service is running;
an exception will be raised if you try to access them out of their contexts.
In nanopie HTTP services, parsed_request includes the headers, query
arguments, and the payload of incoming HTTP requests to a specific endpoint,
deserialized as data model instances in accordance with the data models
specified when creating the endpoint. The code snippet below showcases
how to access the data in requests payloads with the parsed_request
global proxy:
from nanopie import parsed_request
class User(Model):
name = StringField()
age = IntField()
@svc.create(name="create_user",
rule="/users",
data_cls=User)
def create_user():
user = parsed_request.data
print(user.name)
print(user.age)
If the service uses JSON as data interchange format and you call the
create_user endpoint with { "name": "Albert Wesker", "age": 49 }
as payload, you should see following output from the service:
Albert Wesker
49
Note that when deserializing data in HTTP requests based on a data model,
nanopie will ignore the hints and constraints specified in the data model.
Missing fields will be assigned the value None; fields that does not
exist in the data model but exists in the requests will be ignored. As such,
you should use the built-in methods in data model instances to validate the data:
from nanopie import parsed_request
class User(Model):
name = StringField(max_length=20, min_length=1)
age = IntField(maximum=120, minimum=0)
@svc.create(name="create_user",
rule="/users",
data_cls=User)
def create_user():
user = parsed_request.data
try:
user.validate()
except ValidationError:
return "Invalid input"
print(user.name)
print(user.age)
parsed_request proxies an HTTPParsedRequest object. Its attributes are:
| Attributes | Type | Description |
|---|---|---|
headers |
ModelMetaCls |
The headers in the incoming HTTP request parsed as data model instances. |
query_args |
ModelMetaCls |
The query arguments in the incoming HTTP request parsed as data model instances. |
data |
ModelMetaCls |
The payload in the incoming HTTP request parsed as data model instances. |
On the other hand, request proxies an HTTPRequest object that includes
the raw data from the request. Its attributes are:
| Attributes | Type | Description |
|---|---|---|
url |
str |
The URL of the HTTP request. |
headers |
Dict |
The headers of the HTTP request. |
content_length |
int |
The content length of the HTTP request. |
mime_type |
str |
The MIME type of the HTTP request. |
query_args |
Dict |
The query arguments of the HTTP request. |
binary_data |
bytes |
The binary payload of the HTTP request. |
text_data |
bytes |
The text payload of the HTTP request. |
from nanopie import equest
@svc.list(name="list_users",
rule="/users")
def list_users():
print(request.url)
print(request.headers)
print(request.query_args)
print(request.text_data)
Responses
Most microservices and API backends return a resource (or a list of resources)
as responses. For example, a CREATE endpoint usually returns the created
resource and a LIST endpoint all listed resources. In nanopie HTTP services,
to accommodate this pattern, you can directly return nanopie data model
instances from decorated methods as response:
@svc.create(name="create_user",
rule="/users",
data_cls=User)
def create_user():
return User(name="Albert Wesker", age=49)
@svc.list(name="list_users",
rule="/users")
def list_users():
return [
User(name="Albert Wesker", age=49),
User(name="Chris Redfield", age=47)
]
nanopie services will automatically serialize the returned data model instances and build the HTTP response.
Alternatively, you can also use the HTTPResponse class to build
a response manually and return it:
from nanopie import HTTPResponse
@svc.create(name="create_user",
rule="/users",
data_cls=User)
def create_user():
return HTTPResponse(status_code=200,
headers={},
mime_type="application/json",
data='{ "name": "Albert Wesker", "age": 49 }')
The HTTPResponse class has the following arguments:
| Attributes | Type | Description |
|---|---|---|
status_code |
int |
The status code of the HTTP response. |
headers |
Dict |
The headers of the HTTP response. |
mime_type |
str |
The MIME type of the payload in the HTTP response. |
data |
str or bytes |
The payload of the HTTP response. |
Other global proxies
Aside from the request, parsed_request global proxies, nanopie also provides
the following global proxies which you can use:
-
nanopie.svcproxies the service itself (nanopie.services.http.HTTPService), allowing you to read the configuration of the service, such as the default authentication, logging, and tracing handlers.Attributes of
HTTPServiceAttributes Type Description endpointsList[RPCEndpoint]A list of all endpoints. authn_handlerAuthenticationHandlerThe default authentication handler for endpoints. logging_handlerLoggingHandlerThe default logging handler for endpoints. tracing_handlerOpenTelemetryTracingHandlerThe default tracing handler for endpoints. serialization_helperSerializationhelperThe serializationn helper the service uses. max_content_lengthintThe maximum length of requests. -
nanopie.endpointproxies the endpoint (nanopie.services.RPCEndpoint) currently processing requests.Attributes of
RPCEndpointAttributes Type Description namestrThe name of the endpoint. rulestrThe rule associated with the endpoint. entrypointHandlerThe handler used as entrypoint. extrasDictAdditional information about the endpoint.
Running and Testing the service
Once again, as stated in the beginning of the document, using a nanopie
service should not stop you from running your application of your
preferred framework. You can still use the application object as you see
fit; if you plan to use an HTTP server such as
gunicorn for WSGI apps,
it will function normally as well.
-
Using the Flask developmental service
from flask import Flask from nanopie import FlaskService app = Flask(__name__) svc = FlaskService(app=app) @svc.get(name="get_user", rule="/users/<int:user_id>") def get_user(user_id): return "Hello World!" if __name__ == '__main__': # Run the app as configured by the nanopie Flask service # with the Flask developmental server app.run(debug=True, port=5000) -
Using
gunicorn# The Python script above is avaiable at the path `main.py` gunicorn -w 4 main:app