A Quality Python Server In 10 Minutes
Add Your Requests And Go
This is a presentation of a network server. It is intended as a starting point for the implementation of a new, high quality network API. Whether it’s the best possible starting point is open to debate. Criteria for the claim include;
- code clarity
- sophistication
- production readiness
Code clarity gets top billing. This is about making the life of the developer less stressful. Technical details should be contained rather than a dominant presence in the codebase.
Sophistication is about having the tools to do the job. Shortcomings in the networking toolset lead to bandaids. There is no perfect toolset but a more sophisticated toolset should lead to less bandaids. Code clarity trumps sophistication, i.e. a good sophisticated toolset will promote code clarity.
Production readiness refers to capabilities and attributes such as responsiveness, testability, supportability and configurability. The server needs to operate within a target environment; it needs to accept environment configuration, it needs to remain responsive at all times, and it needs to generate a diagnostic record (i.e. logs). Your new server should be a welcome addition to your responsibilities, or to the responsibilities of others.
The Server Code
There are two implementation modules — an API definition and the actual server;
import ansar.encode as ar
__all__ = [
'Multiply',
'Divide',
'Output',
]
class Multiply(object):
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
class Divide(object):
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
class Output(object):
def __init__(self, value=0.0):
self.value = value
ar.bind(Multiply)
ar.bind(Divide)
ar.bind(Output)
Save this API definition in the crunch_api.py
file. Save the following in the crunch_server.py
file;
import ansar.connect as ar
from crunch_api import *
CRUNCH_API = [
Multiply,
Divide,
]
# The server object.
class Server(ar.Point, ar.Stateless):
def __init__(self, settings):
ar.Point.__init__(self)
ar.Stateless.__init__(self)
self.settings = settings
def Server_Start(self, message): # Start the networking.
host = self.settings.host
port = self.settings.port
ipp = ar.HostPort(host, port)
ar.listen(self, ipp, api_server=CRUNCH_API)
def Server_NotListening(self, message): # No networking.
self.complete(message)
def Server_Stop(self, message): # Control-c or software interrupt.
self.complete(ar.Aborted())
def Server_Multiply(self, message): # Received Multiply.
value = message.x * message.y
response = Output(value=value)
self.reply(response)
def Server_Divide(self, message): # Received Divide.
value = message.x / message.y
response = Output(value=value)
self.reply(response)
# Declare the messages expected by the server object.
SERVER_DISPATCH = [
ar.Start, # Initiate networking.
ar.NotListening, # Networking failed.
ar.Stop, # Ctrl-c or programmed interrupt.
CRUNCH_API, # Network API.
]
ar.bind(Server, SERVER_DISPATCH)
# Configuration for this executable.
class Settings(object):
def __init__(self, host=None, port=None):
self.host = host
self.port = port
SETTINGS_SCHEMA = {
'host': str,
'port': int,
}
ar.bind(Settings, object_schema=SETTINGS_SCHEMA)
# Define default configuration and start the server.
factory_settings = Settings(host='127.0.0.1', port=5051)
if __name__ == '__main__':
ar.create_object(Server, factory_settings=factory_settings)
You are good to go. To run this script, execute the following commands;
$ python3 -m venv .env
$ source .env/bin/activate
$ pip3 install ansar-connect
$ python3 crunch_server.py --debug-level=DEBUG
[00205791] 2024-09-19T13:28:32.162 + <00000010>SocketSelect - Created by <00000001>
[00205791] 2024-09-19T13:28:32.162 < <00000010>SocketSelect - Received Start from <00000001>
[00205791] 2024-09-19T13:28:32.162 > <00000010>SocketSelect - Sent SocketChannel to <00000001>
[00205791] 2024-09-19T13:28:32.162 + <00000011>PubSub[INITIAL] - Created by <00000001>
[00205791] 2024-09-19T13:28:32.162 < <00000011>PubSub[INITIAL] - Received Start from <00000001>
[00205791] 2024-09-19T13:28:32.162 + <00000012>object_vector - Created by <00000001>
[00205791] 2024-09-19T13:28:32.162 ~ <00000012>object_vector - Executable "/home/flynn/crunch_server.py" as object process (205791)
[00205791] 2024-09-19T13:28:32.162 ~ <00000012>object_vector - Working folder "/home/flynn"
[00205791] 2024-09-19T13:28:32.162 ~ <00000012>object_vector - Running object "__main__.Server"
..
The server is ready and accepting connections from clients. You can leave off the --debug-level
if you prefer silent running and a control-c will terminate the server.
To try out the new API we can use the curl
command from another shell;
$ curl -s "http://127.0.0.1:5051/Multiply?x=1.5&y=2.25" | jq '.value[1].value'
3.375
$ curl -s "http://127.0.0.1:5051/Divide?x=1.5&y=2.25" | jq '.value[1].value'
0.6666666666666666
The new server has successfully received a Multiply
1.5 by 2.25 and responded correctly with 3.375. Switching to the Divide
request produces the expected 0.67. The jq
utility was used to trim down the full JSON response to the essential result.
The Ansar library has been used as the networking toolset. As well as providing the networking sophistication it also provides several of the features that make the server production ready, e.g. the crunch_server
is configurable, it remains responsive at all times (built in control-c handling) and it generates a diagnostic record. It is also testable using a readily available utility.
Ansar-based applications are supported on several distributions of Linux, macOS 14 and Windows 11. Process orchestration and other features are also supported through the
ansar
CLI. Due to technical differences in these platforms, the latter is not supported on Windows.
Making It Yours
The server is ready for your modifications. Create your own API definition module with your own set of request classes. If you have more complex members (e.g. datetimes
, lists, maps, etc) in your requests you may need to read the following section on how to fully define a request class. The list of request classes needs to be maintained in the CRUNCH_API[]
list (i.e. or your equivalent). Otherwise, it’s all about the implementation of your handlers;
class Square(object):
def __init__(self, x=0.0):
self.x = x
..
..
def Server_Square(self, message)
x = message.x
response = Output(x * x)
self.reply(response)
If your networking requirements exceed the scope of the template provided by crunch_server
you may want to investigate the full capabilities of the Ansar library. Documentation is available through the PyPI page. To answer some of the smaller questions and perhaps whet the appetite, continue reading.
Registration Of Classes
Ansar needs type information about each class and its members — sometimes the information available from Python is not enough. For the simple types this can be extracted from the default value x=0.0
, but for more complex types explicit member declarations will be needed.
PERSON_SCHEMA = {
'person_id': ar.UUID,
'give_name': str,
'family_name': str,
'dob': ar.WorldTime,
'recent': ar.DequeOf(float),
..
}
ar.bind(Person, object_schema=PERSON_SCHEMA)
Ansar supports a rich set of builtin types (e.g. datetimes, enumerations and UUIDs) and also generic container types (e.g. sets, deques and maps). Reference information can be found here. Note that as your types become more complex it also becomes more appropriate to use the application/json
content type in associated HTTP requests.
Responses are also declared as classes — e.g. Output
. This is to allow the request handlers to send back whatever is appropriate to each handler. The full JSON received by the client includes the path of the response class, i.e. “crunch_api.Output”
so that clients can distinguish between different potential responses.
Why The Server Class
Ansar supports several different approaches when starting a new process, including the traditional def main()
, function-based approach. The class-based approach just happens to match what network API servers are all about — dispatching inbound requests to dedicated handlers. So for economy of implementation there is the class Server
.
Explaining the role of classes like Point
and Stateless
and the function create_object
is outside the scope of this document. Suffice to say that these are part of a fully asynchronous runtime provided by the Ansar library.
Requests are delivered to the handlers asynchronously and handlers are expected to behave in an asynchronous fashion. In more practical terms this means that all requests pass through a queue and the handlers should be short and quick.
The presence of the queue is generally good in that it serves as a buffer between the low-level activities around sockets and the high-level activities at the handler level.
Short and quick means avoiding blocking operations where possible. File I/O is fine though the responsibility for reading and/or writing large files in the middle of a heavily used handler stays with the developer.
Traditional blocking calls (i.e. RPCs) to supporting network APIs may cause problems in times of heavy load, or if the supporting service is somehow compromised. Fortunately, Ansar was designed for exactly these scenarios. The catch is that it requires specific coding that deserves its own tutorial (i.e. Python Networking On Steriods). Upgrading from an RPC to the technique described in that tutorial is a judgement call.
Start The Networking
The Server_Start
handler executes in response to a standard message sent by Ansar as part of its asynchronous obligations. The handler takes the opportunity to initiate the network listen. Passing the self
parameter arranges for all the messages (i.e. requests) arriving from clients to be routed to the Server
object.
Ansar also generates session-related messages such as Accepted
, Closed
and Abandoned
. These are ignored by this server module but can be used as the trigger for any required application activity. A scan of the logs generated by the server will find entries related to these session messages and will verify the fact that they are being silently dropped.
All the messages associated with all connected clients are routed to the single instance of a Server
. This is a perfectly valid arrangement, but it is also possible to arrange for the creation of a session object every time a connection is Accepted
. Messages arriving over such connections are routed to the session rather than the Server
.
Receiving Requests And Responding
The Ansar library takes care of all sockets-related details, block I/O, the encoding and decoding of HTTP messages and the transformation of HTTP messages into the classes declared in CRUNCH_API[]
.
The detection of a request results in a call to one of the request handlers and eventually these handlers make a call to self.reply(response)
. This arranges for the specified message to be sent to the current return address, i.e. the client that sent the request. This discreetly takes care of the fact that the Server
may be connected to multiple clients.
The HTTP-based messaging capabilities in Ansar were implemented for the purpose of integration. By default Ansar uses its own messaging protocol that is fully asynchronous and bi-directional.
Terminating The Server
The control-c or SIGINT mechanism is used to initiate termination of the server. Ansar catches the low-level signal and turns this into an ar.Stop
message that propagates through the application. Eventually this results in a call to Server_Stop
. This handler calls self.complete
resulting in the termination of the Server
. In turn, this results in the termination of the process. The ar.Aborted
message passed to self.complete
becomes the output of the process and when executed from the command line, that message is converted into a diagnostic;
..
[00338469] 2024-09-23T21:02:15.965 < <00000011>PubSub[NORMAL] - Received Stop from <00000001>
[00338469] 2024-09-23T21:02:15.965 X <00000011>PubSub[NORMAL] - Destroyed
[00338469] 2024-09-23T21:02:15.965 X <00000010>SocketSelect - Destroyed
crunch_server: aborted (user or software interrupt)
$
A similar process occurs where there is some problem during startup. If the Server_NotListening
handler is called the message parameter is passed on to self.complete
. This time the diagnostic might be something like;
..
[00338596] 2024-09-23T21:06:58.028 < <00000011>PubSub[NORMAL] - Received Stop from <00000001>
[00338596] 2024-09-23T21:06:58.028 X <00000011>PubSub[NORMAL] - Destroyed
[00338596] 2024-09-23T21:06:58.029 X <00000010>SocketSelect - Destroyed
crunch_server: cannot listen at "127.0.0.1:5051" ([Errno 98] Address already in use)
$
Configuring The Server
At the point where the server is started using ar.create_object
, an instance of Settings
is passed as the named parameter factory_settings
. The presence of a non-null value for factory_settings
activates the file-based settings machinery within Ansar. It also causes the passing of the settings value to the Server.__init__
function.
A location and name is derived from the current context resulting in a name such as /home/nigel/.ansar-tool/settings/crunch_server.json
. If the named file doesnt exist it is initialized with the contents of factory_settings
. Changes are effected through the command line;
$ python3 crunch_server.py --host=0.0.0.0
This command causes the server to listen at the new address 0.0.0.0:5051
(the port is unchanged). This is a transient change and does not update the persisted settings;
$ python3 crunch_server.py --dump-settings
{
"value": {
"host": "127.0.0.1",
"port": 5051
}
}
To make changes permanent just add the explicit instruction;
$ python3 crunch_server.py --host=0.0.0.0 --store-settings
Ack()
The server terminates immediately after saving the new settings. It does not proceed to normal operation and the Server
class is never instantiated. It also prints the Ack()
as confirmation that the update was successful.
Storing Logs For Convenient Analysis
A command such as python3 crunch_server.py --debug-level=DEBUG
will cause the printing of logs on stderr
. The DEBUG
enumeration effectively requests all the available logs. This style of live streaming is most obviously useful in a development context.
To direct logging into a storage area requires a bit more setup and different commands are required for the starting and stopping of the server. The following setup creates an operational context for the server;
As mentioned previously, the
ansar
CLI is not supported on the Windows platform
$ source .env/bin/activate
$ pip3 install pyinstaller
$ pyinstaller --onefile --hidden-import=_cffi_backend --log-level ERROR -p . crunch_server.py
$ ansar create
$ ansar deploy dist
$ ansar add crunch_server --role-name=crunch_server
$ ansar list
crunch_server
A folder has been created in the current location (i.e. .ansar-home
) and populated with operational materials. To start the server use;
$ ansar start
$ ansar status --long-listing
crunch_server <315996> (315985) 4m22.8s
The crunch_server
is now running as a daemon process and within the context of the .ansar-home
folder. The extended status listing shows the associated process ids and the total runtime of the server. To extract logs from the server use;
Activity from the last 5 minutes of the crunch_server
process is printed on stdout
. The log
sub-command provides several options mostly about selection of a time window of logs. The command ansar log crunch_server --back=1d --count=100
jumps back 24 hours and prints the first 100 logs.
Logs are self-maintaining and by default storage is capped at 1Gb. As the total quantity of logs nudges this figure, older logs are automatically deleted.
To stop the server use ansar stop
and to remove the inactive context use ansar destroy
.
Complete information on the ansar
utility and details about what appears in each log entry can be found here and here, respectively.
Summary
The original claim was that the crunch_server
was the best way to write your next network API.
The example code is a clear and reliable template for the creation and subsequent maintenance of a network API. It pushes technical details out of the application code, making it less likely that inadvertent editing creates a bug in those areas.
Through its asynchronous roots, Ansar provides a basis for implementation of truly advanced networking. As the requirements placed on your server become more demanding, those roots become more relevant.
The crunch_server
would appear to demonstrate a good way to write your next network API, but the real test is whether other developers feel the same way.