Abstract: This article takes you through the development and testing of
a simple external using the new External Development System. No IDE
is needed; only a text editor and Hermes BBS.

Introduction

The Python-based External Development System was designed from the very
beginning to make it as easy as possible to write externals for the
Hermes BBS. I wanted anyone to be able to write an external, even if
they had never written one using the old APIs.

To accomplish the goal, I built a clean, full-featured API that does all
of the “heavy lifting” for you. Loading and saving of preferences, user
data, statistics, etc. is all taken care of by the API. Menu display
and processing is also handled by the API. All of this allows the
external developer to focus on the code for their external without
worrying about the behind-the-scenes details.

This article shows you how to get started with the new External
Development System. You can make the most of this document by
installing Hermes on your computer and following along with the
examples in this article.

Folder Structure

Classic Externals (those written before the EDS was available) were
traditionally written in Pascal and compiled into a Mac OS code
resource. Static data such a strings or menu text could be included
directly in the source code or stored in the external’s resource fork.
Data for the external (preferences, high scores, user information, etc.)
could be stored wherever the external desired.

All of this has changed with the new Python-based API. Classic
externals were usually shipped as a single file that contained all of
the compiled code and data for the external. Python externals, on the
other hand, are shipped as a directory of source files and resource
files.

This directory does not store any of the external’s runtime data such
as preferences or user information. That information is stored by the
Hermes Python Runtime and can be accessed using a number of simple APIs
(described in the article on Data Properties).

File Types

There are two types of files contained within an external’s bundle
directory:

Python source files (also known as “Python modules”).

Resource files.

Before we discuss the directory layout itself, let’s talk about these
file types.

Python Modules

Python modules are stored in text files that end in “.py”. At minimum,
the Hermes Python Runtime requires a main module. In Python terms,
this would be stored in the main.py file. This is the file that
will be loaded and executed when a user enters your external from the
BBS.

You can divide the source for your external into as many – or as few –
Python modules as you wish. This is purely an architectural
consideration and does not affect the operation of your external. The
only requirement is that you supply a main module that Hermes can
use to start your external.

My recommendation is to start out with a single module and refactor your
source code into multiple modules as your external gets more complex.

Resource Files

Resource files – not to be confused with Mac OS resource forks –
contain the static text and data for your external. There are three
basic types of resource files:

Text files: These contain ASCII text or ASCII and ANSI versions
of the text. If you include both versions of the text in your file,
the BBS will automatically select the appropriate format based on
the capabilities of the user’s terminal.

String lists: Each line in a string list resource contains a key
and associated value. The key can be any alphanumeric string.
String lists are useful for storing lists of configuration values
that you might want to change during the development of the
external. Keeping these values in a separate file (rather than in
Python source code) makes it easier to maintain both your Python
code and the configuration data for your external. Leech 2000
uses string lists to store the information for each of the software
types in the game. String lists are also used to store the
in-game BBS List. If desired, a SysOp could customize (or even
localize!) these files for their BBS.

Menu files: The most complex – and one of the most useful –
resource types is the menu description file. These files let you
define both the layout of your menu as well as the functions that
are activated by each of the user’s key presses. You could store
this information in a text file and implement the menu yourself, but
using menu description files lets you change the menu without
changing your Python code.

Directory Layout

Every external built with the External Development System is stored in a
directory or bundle. The name of this directory is the name that will
be displayed for the external in the BBS’ external menu.

At minimum, this directory must contain a file called main.py. This
is the file that will be loaded by the Hermes Python Runtime to start
your external when a user selects it from the external menu. You can
have other Python files in this directory (or even entire Python
packages if you want), all of which must end in “.py”.

It is important to note that the external’s directory is not a Python
package. No __init__.py file is necessary. Hermes constructs a
Python interpreter specifically for your external and runs it directly
from your external’s directory.

Your resource files are also stored in the external’s bundle, although
they are located in a directory called resources and further segmented
by type. Text files are in the text directory, string lists are in
the strings directory, and menu files are in the menu directory.

Let’s take a look at the directory layout for Leech 2000 to see how a
complex external might be organized:

As you can see, the Leech 2000 source code is split into four Python
modules: the main module and three other modules that contain the
code for various portions of the game. There is a single menu resource,
three string resources, and four text resources.

The MainMenu is stored in a text resource instead of a menu resource
so that I could maintain the DOS-style main menu from the original Leech
game. In Leech (and Leech 2000) you have to type in the complete text
of the menu item at the main menu. To implement this, I stored the text
of the menu itself in a text resource and decode the menu commands in
the main.py file.

Leech 2000’s main menu is an anomaly; you would rarely need to implement
a menu this way. Most BBS menus use a single letter (or number) to
activate a menu item. Menu resources were designed to support those
kinds of menus and make them easy to implement. For a better example of
menu handling, see the code for the LLL menu in the LLL.py file.

Simple externals, such as the Hello Hermes World described in the next
section, may only contain a single file in their bundle: the main.py
source file.

“Hello Hermes World”

This section of the article takes you through the design and
installation of a simple “Hello World” external. Ours is, of course,
called “Hello Hermes World”.

Step 1: Create the bundle

Make a directory called Hello Hermes World in the Externals
folder of your Hermes BBS. This folder is located in the Hermes
Files folder.

Step 2: Create the main.py file

Open up your new Hello Hermes World directory and – using your
favorite text editor – create a file called main.py inside of this
directory. Make sure that you understand how Python uses indentation
before continuing. You need to decide if you will use spaces or tabs
(PEP 8 recommends spaces) when writing your Python source and then
consistently apply that decision throughout your Python career.

Type the following code into your main.py file:

fromhermesimport*

print'Hello Hermes World!'

This code is pretty simple, which is of course the whole point of the
new External Development System. Save the file and close your text
editor.

Step 3: Set access permissions

Load up Hermes, log in, and go to the external menu. You may or may not
be surprised to find out that your external is not in the menu. This
is because the access permissions for the external have not yet been
set. By default, all Python externals are hidden from all users
(including the SysOps) until the SysOp has configured the access
permissions for the external.

In the old days you would use a program called SetHRMS to set the
minimum security level (SL) and optional access letter for each
external. Python-based externals do not have a resource fork, so this
method cannot be used.

Instead, EDS externals use a “magic directory” to specify access
permissions. This directory is placed in the Externals folder along
with the external bundle, but has a special name that sets the
permissions for the external. The directory itself is empty, only the
name is important.

The format of the magic directory name is as follows:

The name of the external.

A semicolon (;)

The minimum security level needed to access the external.

An optional access letter, which must be separated from the security
level by a semicolon.

If you want to restrict access to Hello Hermes World to users with an SL
of 10 or higher, you would create an empty directory named Hello
Hermes World;10 and put it in the Externals directory along with
the Hello Hermes World bundle.

Here are some sample magic directory names:

Hello Hermes World;250 - The user must have a security level of
250 or higher.

Hello Hermes World;10;X - The user must have a security level of
10 or higher and have the X access letter in their security
profile.

These magic directory names are invalid:

Hello Hermes World;A - Must specify a security level, not just
an access letter.

Hello Hermes World;300 - Security levels must be 1-255.

Hello Hermes World;10;ABC - Can only specify one access letter.

Create a magic directory for your external (use Hello Hermes
World;255 if you only want it to be accessible by SysOps) and put it
in the Externals folder. Quit the BBS and restart it (externals and
magic directories are only loaded when the BBS starts up), log in, then
take a look at the external menu. Your external should appear in the
menu.

If you run your external, it should say “Hello Hermes World” and then
return you to the external menu.

Step 4: Make some changes

While you are logged in to the BBS, and while you are still sitting at
the external menu, open the main.py file back up in your text editor
and change the file to read:

fromhermesimport*

print'Hello Hermes World!'print'More exciting text.'

Save the file and select your external from the external menu. The new
text appears! You did not have to restart the BBS, or even log off to
get this new functionality. Python externals are loaded each time the
user enters the external; you can do all of your development without
ever logging off from the BBS.

It is important to note that any changes you make while a user is
accessing the external will not take effect until the next time that the
user enters the external. In other words, you must quit the external,
then re-run it from the external menu after making changes to the source
code.

The exception to this rule is resource files. Resource files are loaded
every time they are accessed. For example, every time a menu is
displayed the resource file for that menu is re-read. This allows you
to modify menus, string lists, and text files while the external is
running. This is especially helpful when you are designing ANSI menus:
just keep refreshing the menu while you make changes to the resource
file in your text editor.

Using the Hermes Python Console

This web site contains a number of documents and articles that explain
the Hermes API. Although you could read all of these documents, then
sit down and write an external from scratch, some aspects of the new
system are easier to play with than to read about. The Hermes Python
Console gives you a way to interactively try out different portions of
the API. You can even use the Console to test and debug your external
as we will see later on.

Interacting with the API

One of the primary uses of the Hermes Python Console is to allow
external developers to interactively experiment with the Hermes API.
Python encourages exploratory programming and this is a concept that I
carried forward into the Hermes API.

This section of the article will take you through the development of the
Update Phone external. Instead of providing the entire source file as
was done with the Hello Hermes World external, I will instead guide you
through the development of the external using the Hermes Python Console.

Exploring the user object

Our Update Phone External will do three things: display the user’s
current phone number, ask them if they want to change their phone
number, and then update the user’s phone number if they answer “Yes” in
step 2. Let’s tackle the first step: retrieving and displaying the
user’s phone number.

Log on to your BBS and start the Hermes Python Console. You will be
greeted with the welcome banner and given a Python prompt:

HermesPythonConsolev1.0--type"exit"whendone>>>

Every Python-based Hermes external needs to import the hermes module in
order to get access to the BBS’ data structures, the Hermes API prompts,
and other functionality provided by the External Development System.
Import the hermes module into the Console:

>>>fromhermesimport*>>>

The Python interpreter has now brought all of the functions, constants,
and global variables into the Console’s namespace. You can confirm this
by asking for the value of the user object:

>>>user<userMichaelAlynMiller>>>>

Since I am the one currently accessing the external, the Console prints
out a Python object that references my user information. We can use
this user object to get additional information about my account:

Looks good. Now on to the next step: asking the user if they want to
change their phone number.

Trying out prompts

The hermes module provides a number of different types of prompts that
you can use to interact with the user. Under the covers, all of these
prompts are implemented by using the stdin and stdout objects stored
in Python’s sys module. You could read and write those objects
directly, but the prompt functions defined in the hermes module take
the drudgery out of prompting the user for information.

There are two types of prompts that we will need for our Update Phone
external: a yesNoPrompt to ask the user if they want to change their
phone number and a textPrompt to retrieve the new phone number.

Let’s try these prompts out in the Hermes Python Console. As always,
begin by importing the hermes module if you have not already done so.
Here is the yesNoPrompt in action:

The prompt returns 0 (which is the same as False) if the user
responds with “No” and 1 (or True) if the user responds with
“Yes”. The yesNoPrompt supports a number of additional arguments, but
at a minimum it only requires the prompt text. If we wanted the prompt
to default to “No” (if the user hits Return instead of pressing
Y or N), then we would use the defaultValue argument:

In this example, I just hit Return and the prompt automatically
selected the “No” option. Notice that the defaultValue argument
does not take the character that will be entered, but the actual value
(True or False) that the prompt should return if the user hits
Return.

Now that we can ask the user if they want to change their phone number,
let’s explore the textPrompt function that will let us get the new
phone number:

This is not very helpful. Let’s use some of the extra arguments to the
textPrompt function to force the user to enter only those characters
that would appear in a normal phone number. We will also increase the
maximum response length so that international numbers can be entered.
Let’s try out the new arguments:

We still don’t enforce the format of the number, but at least the
characters will be valid. This is good enough for now, but feel free to
explore the implementation of the hermes module if you would like to
write a prompt specifically for phone numbers.

Putting it all together

We now have enough information to write our Update Phone external. Here
is what the completed external looks like:

# See if they want to change their phone number.ifyesNoPrompt('Change your phone number? ',defaultValue=False):# Get the new number.newPhone=textPrompt('New phone number: ',24,validChars='0123456789-+() ')

# Change the phone number if the user typed something into# the prompt.ifnewPhone:user.phone=newPhoneprint'Your phone number has been changed.'else:print'Your phone number has not been changed.'

The next section of this article takes a look at how to interactively
test and debug an external using the Hermes Python Console.

Testing and debugging

Simple externals such as Update Phone require little testing. There are
only a handful of different code paths, all of which can be easily
tested by running the external. More complex externals benefit from an
interactive testing and debugging approach, especially when
user-specific variables get involved.

The new Update Phone

This section of the document will show you how to test the functionality
of an external using the Hermes Python Console. We will again use the
Update Phone external, but I have modified it to make it both easier to
test and easier to read:

# Import the hermes module.fromhermesimport*

defchangePhoneNumber():"""Asks the user for their new phone number and updates
their user record if we get a valid response."""

# Get the new number.newPhone=textPrompt('New phone number: ',24,validChars='0123456789-+() ')

# Change the phone number if the user typed something into# the prompt.ifnewPhone:user.phone=newPhoneprint'Your phone number has been changed.'else:print'Your phone number has not been changed.'

# See if they want to change their phone number.ifyesNoPrompt('Change your phone number? ',defaultValue=False):changePhoneNumber()

if__name__=='__main__':main()

A couple of changes have been made to the source code. First, the
external no longer runs as soon as it has been imported. The contents
of Python’s __name__ global variable is checked and the code for
the external executes only if this value is set to __main__. (See
the Python documentation for more information about this idiom.)

The code has also been split into two functions: one that implements the
main loop of the external (the main function) and one that changes
the user’s phone number (the changePhoneNumber function). The
second function is not really necessary, but it will make it much easier
to demonstrate the interactive debugging features of the Hermes Python
Console.

Create an external bundle called New Update Phone and save the
Python source code above in a file called main.py. Create the magic
directory for New Update Phone as well. Restart the Hermes BBS and make
sure that the external works.

Testing Update Phone

The Hermes Python Console is an external just like the (New) Update
Phone external. But the Console is unique in that it gives you a way to
interactively manipulate the Python runtime environment. We are going
to use this functionality to call into different parts of our New Update
Phone external.

In order to do this, the code for New Update Phone must be available to
the Hermes Python Console. Python externals can only access code in
their own external bundle, so we need to copy the main.py file from
the New Update Phone bundle into the Hermes Python Console. The Console
external already has a main.py file, so we will need to call the new
file something else. Create a copy of New Update Phone’s main.py
file, rename it to nup.py, and move the copy into the Hermes Python
Console bundle.

Enter the Hermes Python Console and import the New Update Phone module:

HermesPythonConsolev1.0--type"exit"whendone>>>importnup>>>

The contents of the nup module have now been loaded into the
Console’s namespace. Because we loaded this module interactively
(instead of running it from the external menu) the __name__ global
variable is not set to __main__. As a result, the code for the New
Update Phone external was not executed.

Notice that we were not asked if we wanted to change our phone number.
This is because the main method was never run: we jumped directly to
the changePhoneNumber method.

Manipulating data properties

The ability to interactively test an external is especially valuable
when your external makes use of data properties. In these cases, the
output of an external may depend heavily on the current state of the
user or instance objects.

Leech 2000 stores a number of data properties about the user. One of
these is the user’s current level in the game. The user begins at level
1; at level 31 they are forced to battle the LLL computer. There are
two ways that I could have tested the “final battle” code:

Play the game until I reach level 31. Fun, but time consuming.

Use the Hermes Python Console to “play” Leech 2000, manipulating the
game’s data properties as necessary.

I chose the second option. Here is how I did it:

Copy all of Leech 2000’s .py files as well as its resources
folder into the Hermes Python Console’s external bundle. I renamed
Leech 2000’s main.py file to leech2000.py in the process.

Start the Hermes Python Console and import the Leech 2000 code:

HermesPythonConsolev1.0--type"exit"whendone>>>importleech2000>>>

Play Leech 2000 once inside of the Hermes Python Console environment
so that all of the game and user data is initialized:

>>>leech2000.main()

(Leech2000entry,menu,etc.)

>>>

All of the {bbs,prefs,user}.data.* properties for an external
are stored in a special storage area that is based on the name of
the external that is running. Even if you had played the real Leech
2000 game, none of that data would be available to the Leech
2000 game that is being tested in the Hermes Python Console.

Playing Leech 2000 once in the Hermes Python Console ensures that
the game’s data properties are initialized before we start editing
properties. We also need a user object to edit, so creating a
Leech 2000 player serves a dual purpose.

Now that I have a Leech 2000 character, I can manually set my
user.data properties so that I am a level 31 Leech:

I gave myself the most powerful software and backup type as well as
plenty of megs. This way I could make sure to beat the LLL
computer. I also tested Leech 2000 with the minimum values to
ensure that the failure scenario worked correctly as well.

Play Leech 2000 again and make sure that the final battle code works
as expected:

>>>leech2000.main()

(Leech2000entry,finalbattle,etc.)

>>>

This made it much easier to test Leech 2000 and ensured that a user
wouldn’t get all the way to level 31 only to find a bug in a piece of
code that I had never run before.

Testing this way is especially important in Python, which is a
dynamically-typed language. Certain types of software bugs will not be
detected until the first time that a function is run. You want to find
these sorts of errors during the development and testing phase, not
when one of your users is playing the game.

A couple of final notes regarding this testing method:

Although I called into the main function, I could just as easily
have called the functions that manage the final battle. It was
easier to enter the game in the normal way, so that is what I did.

You should clear the data properties for the Hermes Python Console
before and after running tests like this. You do not want the data
for an old test interfering with your current test. The easiest way
to clear the data is to remove the Hermes Python Console.data
file in the Temp directory of the Hermes Files folder. Only
do this when the external is not running.

Changes to any of the .py files that you are testing will not
take effect until the next time you re-enter the Hermes Python
Console external.

The Hermes Python Console makes it much easier to test externals,
utility functions, or just to prototype an idea. I recommend that you
spend some time playing with the Console as you write your first
external. I am sure you will find it to be an invaluable tool.

What Next?

Hopefully this document has given you a good understanding of the
External Development System and the new, Python-based Hermes API. Here
are some next steps for you to consider:

Browse through the Leech 2000 source code and see how the game
makes use of the different prompting and resource functions. Pay
careful attention to the use of the data properties. Try making
changes to the Leech 2000 code to add more features, additional
menus, etc.

Spend more time with the Hermes Python Console, trying out all of
the different functions in the hermes module.

Write your own external! You might have a game or utility that you
want to build, or perhaps you want to re-implement one of the
classic BBS games.

Thanks for taking the time to read this article. As always, please feel
free to contact me if you have any comments or questions about this
document.