Selected Recipes in Python

Although we have used Python for our examples, you can easily adapt these recipes to command-line scripts written in other programming languages.

Write configuration file

You have a command-line script that you want to turn into a web application. Here is an example of such a script.

from sys import argv

x, y = argv[1:]
print('{} divided by {} is {}'.format(x, y, float(x) / float(y)))

Running the script produces the following output.

$ python run.py 4 5
4 divided by 5 is 0.8

To wrap the script in a web interface, we write a configuration file.

[crosscompute]
command_template = python run.py {x} {y}
show_raw_output = True

x = 10
y = 3

Specify tool name

The configuration file must end with the extension .ini and contain a section that starts with the word crosscompute, optionally followed by the name of the tool.

[crosscompute our-simple-one-function-calculator]

If you do not specify a tool name, then the name of the tool will be the name of the folder containing the configuration file.

Specify command-line arguments

The most important option in the configuration file is the command_template, which tells CrossCompute how to run your script.

Here, we use python to execute run.py with x and y as arguments.

command_template = python run.py {x} {y}

If your command is long, you can split it across multiple lines.

command_template = bash script-with-many-arguments.sh
    {first_argument}
    {second_argument}
    {third_argument}

Capture raw output

CrossCompute parses but does not save raw output from the script, unless requested to do so explicitly.

show_raw_output = True

Specify default values for arguments

When executed without arguments, crosscompute run uses the default values specified in the configuration file, which can save time during development.

x = 10
y = 3

Additionally, crosscompute serve uses the default values to populate the tool form. If an argument name ends with _path and a default file path is specified, the web app will show the contents of the file in the form. For example, this configuration file

[crosscompute]
command_template = python run.py {a_text_path}
a_text_path = cc.ini

will render the following form.

../_images/get-size-tool.png

Run tool

First, check that the application development framework is installed on your system.

$ crosscompute
usage: crosscompute {serve,run} ...
crosscompute: error: too few arguments

Then execute crosscompute run in the parent folder or same folder as your configuration file.

$ crosscompute run
[tool_location]
configuration_path = ~/Projects/crosscompute-docs/examples/python/divide-floats/cc.ini
tool_name = divide-floats

[result_arguments]
x = 10
y = 3

[raw_output]
10 divided by 3 is 3.3333333333333335

[result_properties]
raw_output = 10 divided by 3 is 3.3333333333333335
execution_time_in_seconds = 0.04039597511291504
command_path = ~/.crosscompute/divide-floats/results/2/x.sh

If there is more than one tool, you will need to specify the tool name explicitly.

$ crosscompute run divide-floats

Override default values

Sometimes, you might want to override default argument values. Use --help to show the required syntax.

$ crosscompute run --help
usage: divide-floats [-h] [--x X] [--y Y]

optional arguments:
-h, --help  show this help message and exit
--x X
--y Y

If our script terminates unexpectedly, crosscompute run will show the errors. In this case, the exception renders twice because show_raw_output = True.

$ crosscompute run --y 0
[tool_location]
configuration_path = ~/Projects/crosscompute-docs/examples/python/divide-floats/cc.ini
tool_name = divide-floats

[result_arguments]
x = 10
y = 0

[raw_output]
Traceback (most recent call last):
  File "run.py", line 4, in <module>
    print('{} divided by {} is {}'.format(x, y, float(x) / float(y)))
ZeroDivisionError: float division by zero

[result_properties]
return_code = 1
raw_output =
  Traceback (most recent call last):
    File "run.py", line 4, in <module>
      print('{} divided by {} is {}'.format(x, y, float(x) / float(y)))
  ZeroDivisionError: float division by zero
execution_time_in_seconds = 0.14070677757263184
command_path = ~/.crosscompute/divide-floats/results/4/x.sh

Serve tool

Once you are satisfied that the script is configured properly, execute crosscompute serve to serve the web app.

$ crosscompute serve
../_images/divide-floats-tool.png

Click Run to see the result.

../_images/divide-floats-result.png

Start from a scaffold

To save time, you can start building your tool from a pre-defined scaffold, courtesy of Pyramid.

$ pcreate -l
Available scaffolds:
  alchemy:                 Pyramid SQLAlchemy project using url dispatch
  cc-python:               CrossCompute Tool in Python
  ir-posts:                InvisibleRoads Posts
  pyramid_jinja2_starter:  Pyramid Jinja2 starter project
  starter:                 Pyramid starter project
  zodb:                    Pyramid ZODB project using traversal

The cc-python scaffold will clone the basic tool scaffold in Python.

$ pcreate -s cc-python your-tool-name

Here is the basic tool scaffold configuration file.

[crosscompute]
command_template = python run.py
    --target_folder {target_folder}

Here is the basic tool scaffold script.

from argparse import ArgumentParser
from invisibleroads_macros.disk import make_enumerated_folder_for, make_folder
from invisibleroads_macros.log import format_summary


def run(target_folder):
    return []


if __name__ == '__main__':
    argument_parser = ArgumentParser()
    argument_parser.add_argument(
        '--target_folder',
        metavar='FOLDER', type=make_folder)

    args = argument_parser.parse_args()
    d = run(
        args.target_folder or make_enumerated_folder_for(__file__))
    print(format_summary(d))

Save output files

The target_folder argument is a special keyword that specifies the folder where your script can save files for the user to download.

[crosscompute]
command_template = python run.py {target_folder}

If your script saves files in the folder, then the user will have access to those files by downloading the archive on the result page.

from os.path import join
from sys import argv

target_file = open(join(argv[1], 'xyz.txt'), 'w')
target_file.write('Repeatedly try to start productive tasks')

Run the script by typing crosscompute run in the same folder as cc.ini.

$ crosscompute run
[tool_definition]
tool_name = save-text
configuration_path = ~/Experiments/save-text/cc.ini
command = python run.py /tmp/save-text/results/1

[result_arguments]
target_folder = /tmp/save-text/results/1

[result_properties]
execution_time_in_seconds = 0.0495231151581

$ ls /tmp/save-text/results/1
result.cfg  xyz.txt

Serve the script by typing crosscompute serve in the same folder as cc.ini.

$ crosscompute serve

Run the tool, then click Download to receive an archive containing the result configuration as well as any files saved in the target_folder.

../_images/save-text-result.png

Additionally, the tool will be able to render the content of those files for selected data types (see Specify data types for result properties). Here is a slightly more involved example that counts the number of each non-whitespace character in a text file.

[crosscompute]
command_template = python run.py {target_folder} {source_text_path}

source_text_path = cc.ini

The script tells CrossCompute to render the output file as a table by printing a statement in the form xyz_table_path = abc.csv.

import csv
from collections import Counter
from invisibleroads_macros.disk import make_folder
from invisibleroads_macros.text import compact_whitespace
from os.path import join
from sys import argv

target_folder, text_path = argv[1:]
character_counter = Counter(compact_whitespace(open(text_path).read()))
del character_counter[' ']
target_path = join(make_folder(target_folder), 'character_count.csv')
csv_writer = csv.writer(open(target_path, 'w'))
csv_writer.writerows(character_counter.most_common())
print('character_count_table_path = ' + target_path)

Running the tool using crosscompute serve renders the desired table.

../_images/count-characters-result.png

Specify data types for tool arguments

Specifying the data type of a tool argument provides the following benefits.

  • The script can assume that an argument matches its specified data type. For example, the script below can assume that its first argument is an integer because the framework performs basic integer validation before running the script.
  • The corresponding web application renders an appropriate query for the tool argument in the form.
../_images/load-inputs-tool.png

The suffix of a tool argument determines its data type. Specify tool arguments in the command_template by enclosing argument names in curly brackets (see Specify command-line arguments). In the configuration file below, the arguments are some_count (integer), a_text_path (text), a_table_path (table).

[crosscompute]
command_template = python run.py
    {some_count}
    {a_text_path}
    {a_table_path}

some_count = 100
a_text_path = abc.txt
a_table_path = xyz.csv

Only the configuration file command_template is relevant when determining tool argument data types. The script does not have to use the same argument names.

from csv import DictReader
from sys import argv

x, text_path, table_path = argv[1:]
print('x = %s' % x)
print('text = %s' % open(text_path).read().strip())
print('table.columns = %s' % DictReader(open(table_path)).fieldnames)

Install the relevant data type plugins. CrossCompute matches argument name endings to suffixes registered by installed data types.

pip install -U crosscompute-integer
pip install -U crosscompute-text
pip install -U crosscompute-table

You can also register your own data type plugins. For examples on how to write data type plugins, please see https://github.com/crosscompute/crosscompute-types.

Specify data types for result properties

Specifying the data type of a result property provides the following benefits.

  • The corresponding web application renders an appropriate value for the result property in the form.

First, include a target_folder in the command_template.

[crosscompute]
command_template = python run.py {target_folder}

Then, save output files in the target_folder (see Save output files) and print statements to standard output in the form abc_suffix = xyz where the suffix corresponds to the desired data type.

import matplotlib
matplotlib.use('Agg')  # Prevent no $DISPLAY environment variable warning

from invisibleroads_macros.disk import make_folder
from matplotlib import pyplot as plt
from os.path import join
from sys import argv

target_folder = make_folder(argv[1])
# Render integer
print('an_integer = 100')
# Render table
target_path = join(target_folder, 'a.csv')
open(target_path, 'w').write("""\
a,b,c
1,2,3
4,5,6
7,8,9""")
print('a_table_path = ' + target_path)
# Render image
target_path = join(target_folder, 'a.png')
figure = plt.figure()
plt.plot([1, 2, 3], [1, 2, 2])
figure.savefig(target_path)
print('an_image_path = ' + target_path)
# Render geotable (map)
target_path = join(target_folder, 'b.csv')
open(target_path, 'w').write("""\
Latitude,Longitude,Description
27.3364347,-82.5306527,A
27.3364347,-82.5306527,B
25.7616798,-80.1917902,C
25.7616798,-80.1917902,D
""")
print('a_geotable_path = ' + target_path)

The example above contains the following print statements:

print('an_integer = ...')       # Render integer
print('a_table_path = ...')     # Render table
print('an_image_path = ...')    # Render image
print('a_geotable_path = ...')  # Render geoimage (map)

Serve and run the tool to render the result.

$ crosscompute serve
../_images/save-outputs-table.png ../_images/save-outputs-image.png ../_images/save-outputs-geotable.png

Log errors and warnings

There are two ways that you can communicate an error or warning to the user:

  • Option 1: Set show_raw_output = True in the configuration file (see Capture raw output). The advantage is that this does not require changes in the script.
  • Option 2: Print to standard output in the format abc.error = xyz. The advantage is that this provides finer control of the information that you share with the user.
[crosscompute]
command_template = python run.py {x_integer} {y_integer}

x_integer = 7
y_integer = 3
from sys import argv

x, y = map(int, argv[1:])
try:
    print('quotient = {}'.format(x / y))
    print('remainder = {}'.format(x % y))
except ZeroDivisionError:
    exit('divisor.error = cannot divide by zero')
../_images/divide-integers-error.png

Specify help popovers

A help popover is a helpful description that appears when the user touches a question mark icon. To add a help popover to a tool argument or result property, use the following syntax in the configuration file:

your_argument_name.help = helpful description
your_result_property.help = another description

Here is an example configuration file.

[crosscompute]
command_template = python run.py {x}

x.help = independent variable
y.help = dependent variable

And here is the resulting interface.

../_images/show-popovers-result.png

Add descriptions

For longer descriptions and links, you can customize your tool and result interface in Markdown format.

[crosscompute]
command_template = python run.py {amount} {fraction}

amount = 10
fraction = 0.15
fraction.help = Enter a fraction

tool_template_path = tool.md
result_template_path = result.md

Here is the tool template.

# Calculate Tip

{amount: How much was the bill?}

{fraction: How much tip would you like to give?}

Here is the result template.

# Calculated Tip

{tip}

{total}

And here is the resulting interface.

../_images/add-descriptions.png

Serve multiple tools

There are two ways to organize your files when serving multiple tools:

  • Option 1: Have multiple configuration files in separate folders and launch crosscompute serve from the parent folder.

    .
    ├── count-characters
    │   ├── cc.ini
    │   └── run.py
    └── divide-floats
        ├── cc.ini
        └── run.py
    
  • Option 2: Have a single configuration file with multiple sections.

[crosscompute add-numbers]
command_template = python add_numbers.py {a} {b}

[crosscompute subtract-numbers]
command_template = python subtract_numbers.py {a} {b}

Call an external API

If your script requires an API key because it calls an external API, please specify the API key as an environment variable in your script.

from os import environ
environ['GOOGLE_KEY']

The following APIs are supported:

  • GOOGLE_KEY
  • MAPBOX_TOKEN

Show tables

First, make sure you have installed the appropriate data type plugin.

pip install -U crosscompute-table

Then, save the table in target_folder.

from os.path import join
target_path = join(target_folder, 'points.csv')
csv_writer = csv.writer(open(target_path, 'w'))
csv_writer.writerow(['x', 'y'])
csv_writer.writerow([100, 100])

Finally, print the table path to standard output, making sure to specify the data type suffix.

print('point_table_path = ' + target_path)

Here is an example configuration file.

[crosscompute]
command_template = python run.py
    --target_folder {target_folder}
    --point_count {point_count}
    --x_min {x_min}
    --x_max {x_max}
    --y_min {y_min}
    --y_max {y_max}

point_count = 100
x_min = 0
x_max = 100
y_min = 0
y_max = 100

Here is an example script.

import csv
from argparse import ArgumentParser
from invisibleroads_macros.disk import make_enumerated_folder_for, make_folder
from invisibleroads_macros.log import format_summary
from os.path import join
from random import randint


def run(target_folder, point_count, x_min, x_max, y_min, y_max):
    target_path = join(target_folder, 'points.csv')
    csv_writer = csv.writer(open(target_path, 'w'))
    csv_writer.writerow(['x', 'y'])
    for index in range(point_count):
        x = randint(x_min, x_max)
        y = randint(y_min, y_max)
        csv_writer.writerow([x, y])
    return [
        ('point_table_path', target_path),
    ]


if __name__ == '__main__':
    argument_parser = ArgumentParser()
    argument_parser.add_argument(
        '--target_folder',
        metavar='FOLDER', type=make_folder)

    argument_parser.add_argument(
        '--point_count',
        metavar='COUNT', type=int, required=True)
    argument_parser.add_argument(
        '--x_min',
        metavar='FOLDER', type=int, required=True)
    argument_parser.add_argument(
        '--x_max',
        metavar='FOLDER', type=int, required=True)
    argument_parser.add_argument(
        '--y_min',
        metavar='FOLDER', type=int, required=True)
    argument_parser.add_argument(
        '--y_max',
        metavar='FOLDER', type=int, required=True)

    args = argument_parser.parse_args()
    d = run(
        args.target_folder or make_enumerated_folder_for(__file__),
        args.point_count,
        args.x_min,
        args.x_max,
        args.y_min,
        args.y_max)
    print(format_summary(d))
$ crosscompute serve make-points
../_images/make-points-result.png

Show images

First, make sure you have installed the appropriate data type plugin.

pip install -U crosscompute-image

Then, save the image in target_folder. If you are using matplotlib to generate the image, then ensure that the script will run without a display by specifying the Agg backend.

import matplotlib
matplotlib.use('Agg')

from matplotlib import pyplot as plt
from os.path import join

target_path = join(target_folder, 'points.png')
figure = plt.figure()
# Generate your plot here
figure.savefig(target_path)

Finally, print the image path to standard output, making sure to specify the data type suffix.

print('point_image_path = ' + target_path)

Here is an example configuration file.

[crosscompute]
command_template = python run.py
    --target_folder {target_folder}
    --point_table_path {point_table_path}
    --point_table_x_column {point_table_x_column}
    --point_table_y_column {point_table_y_column}

point_table_path = points.csv
point_table_x_column = x
point_table_y_column = y

Here is an example script.

import matplotlib
matplotlib.use('Agg')

from argparse import ArgumentParser
from crosscompute_table import TableType
from invisibleroads_macros.disk import make_enumerated_folder_for, make_folder
from invisibleroads_macros.log import format_summary
from matplotlib import pyplot as plt
from os.path import join


def run(
        target_folder,
        point_table,
        point_table_x_column,
        point_table_y_column):
    xys = point_table[[point_table_x_column, point_table_y_column]].values
    figure = plt.figure()
    plt.scatter(xys[:, 0], xys[:, 1])
    image_path = join(target_folder, 'points.png')
    figure.savefig(image_path)
    return [
        ('points_image_path', image_path),
    ]


if __name__ == '__main__':
    argument_parser = ArgumentParser()
    argument_parser.add_argument(
        '--target_folder',
        metavar='FOLDER', type=make_folder)

    argument_parser.add_argument(
        '--point_table_path',
        metavar='PATH', required=True)
    argument_parser.add_argument(
        '--point_table_x_column',
        metavar='COLUMN', required=True)
    argument_parser.add_argument(
        '--point_table_y_column',
        metavar='COLUMN', required=True)

    args = argument_parser.parse_args()
    d = run(
        args.target_folder or make_enumerated_folder_for(__file__),
        TableType.load(args.point_table_path),
        args.point_table_x_column,
        args.point_table_y_column)
    print(format_summary(d))
$ crosscompute serve show-plot
../_images/show-plot-result.png

Show maps

The geotable data type uses the table name and column names to render the map (see Render geometries).

First, make sure you have installed the appropriate data type plugin.

pip install -U crosscompute-geotable

Then, save a table with spatial coordinates in target_folder. If you are using pandas, then you can use to_csv, to_json, to_msgpack to save in CSV, JSON, MSGPACK format, respectively.

from pandas import DataFrame
target_path = join(target_folder, 'memory.csv')
memory_table = DataFrame([
    ('Todos Santos', 15.50437, -91.603653),
    ('Semuc Champey', 15.783471, -90.230759),
], columns=['Description', 'Latitude', 'Longitude'])
map_table.to_csv(target_path, index=False)

Finally, print the table path to standard output, making sure to specify the data type suffix.

print('memory_table_path = ' + target_path)

Here is an example configuration file.

[crosscompute show-map]
command_template = python show_map.py
    --target_folder {target_folder}
    --map_table_name {map_table_name}
    --map_table_path {map_table_path}

map_table_name = location_pencil_geotable
map_table_path = locations.csv

Here is an example script. Please save it as show_map.py.

from argparse import ArgumentParser
from crosscompute_table import TableType
from invisibleroads_macros.disk import make_enumerated_folder_for, make_folder
from invisibleroads_macros.log import format_summary
from os.path import join


def run(target_folder, map_table_name, map_table):
    target_path = join(target_folder, 'map.csv')
    map_table.to_csv(target_path, index=False)
    return [(map_table_name + '_path', target_path)]


if __name__ == '__main__':
    argument_parser = ArgumentParser()
    argument_parser.add_argument(
        '--target_folder',
        metavar='FOLDER', type=make_folder)

    argument_parser.add_argument(
        '--map_table_name',
        metavar='NAME', required=True)
    argument_parser.add_argument(
        '--map_table_path',
        metavar='PATH')

    args = argument_parser.parse_args()
    d = run(
        args.target_folder or make_enumerated_folder_for(__file__),
        args.map_table_name,
        TableType.load(args.map_table_path))
    print(format_summary(d))

Here is an example table that specifies feature radius and color. Please save it as locations.csv.

ID,Description,Latitude,Longitude,RadiusInPixelsRange10-20FromMean,FillBluesFromSum
A,"Bar Harbor, ME",44.3876119,-68.2039123,3,1
B,"New York, NY",40.7127837,-74.0059413,1,1
C,"New York, NY",40.7127837,-74.0059413,1,1
D,"New York, NY",40.7127837,-74.0059413,1,1
E,"Sarasota, FL",27.3364347,-82.53065269999999,1,1
F,"Sarasota, FL",27.3364347,-82.53065269999999,3,1
$ crosscompute serve show-map
$ crosscompute serve show-map-examples
../_images/show-map-result.png

Note that clicking on a feature in the map will show its attributes in a table.

Render geometries

If the table has a column name ending in _latitude and a column name ending in _longitude, then each row will render as a point in the map.

../_images/show-map-examples-geometry-point.png

If the table has a column name ending in _wkt, then each row will render as the corresponding WKT geometry. Specify WKT coordinates using (longitude, latitude) coordinate order.

../_images/show-map-examples-geometry-wkt.png

Here are the recognized WKT geometry types:

  • POINT
  • MULTIPOINT
  • LINESTRING
  • MULTILINESTRING
  • POLYGON
  • MULTIPOLYGON

Vary background

To change the map background, specify the desired tile layer in the table name.

a_streets_satellite_geotable
an_outdoors_geotable
a_pirates_geotable
../_images/show-map-examples-background.png

Here are the available backgrounds, courtesy of Mapbox:

  • streets
  • light
  • dark
  • satellite
  • streets-satellite
  • wheatpaste
  • streets-basic
  • comic
  • outdoors
  • run-bike-hike
  • pencil
  • pirates
  • emerald
  • high-contrast

Specify radius

If the table has a column that starts with RadiusInMeters or radius_in_meters or radius-in-meters or radius in meters or some variation thereof and if the row is a point geometry, then the value for the row in that column will render as the point radius in meters. Use this setting if it is important to visualize the real-world radius of each point.

../_images/show-map-examples-radius-meter.png

If the table has a column that starts with RadiusInPixels, radius_in_pixels, radius-in-pixels or radius in pixels and if the row is a point geometry, then the value for the row in that column will render as the point radius in pixels. This setting ensures that the point will remain the same size on the screen independent of the map zoom level.

../_images/show-map-examples-radius-pixel.png

Sometimes it can be convenient to scale the radius to a specific range. Use the syntax RadiusInPixelsRange10-100, radius_in_pixels_range_10_100, radius-in-pixels-range-10-100 or radius in pixels range 10 100.

../_images/show-map-examples-radius-pixel-range.png

If there are multiple rows for a given geometry, then you can specify how to combine the values to compute the radius.

RadiusInPixelsFromMean
RadiusInPixelsRange10-100FromMean
RadiusInPixelsFromSum
RadiusInPixelsRange10-100FromSum
../_images/show-map-examples-radius-pixel-mean.png ../_images/show-map-examples-radius-pixel-sum.png

Specify fill color

Add a column named FillColor to specify the fill color of the geometry, courtesy of the Matplotlib color module. Here are examples of valid values in the FillColor column.

# b blue, g green, r red, c cyan, m magenta, y yellow, k black, w white
r

# gray shade specified as decimal between 0 and 1
0.1

# hex string
#ff565f

# color name
purple

If there are multiple rows for a given geometry, then you can specify how to combine the values to compute the fill color.

FillColorFromMean
FillColorFromSum
../_images/show-map-examples-specific-color-mean.png ../_images/show-map-examples-specific-color-sum.png

Use color scheme

If the column name starts with Fill, followed by the name of a recognized color scheme, then the geometry will normalize and render the value for the row in that column to the specified color scheme.

../_images/show-map-examples-color-scheme-red.png ../_images/show-map-examples-color-scheme-blue.png

Here are the recognized color schemes, courtesy of ColorBrewer:

  • blues
  • brbg
  • bugn
  • bupu
  • gnbu
  • greens
  • greys
  • oranges
  • orrd
  • paired
  • pastel1
  • piyg
  • prgn
  • pubu
  • pubugn
  • puor
  • purd
  • purples
  • rdbu
  • rdgy
  • rdpu
  • rdylbu
  • rdylgn
  • reds
  • set1
  • set3
  • spectral
  • ylgn
  • ylgnbu
  • ylorbr
  • ylorrd

If there are multiple rows for a given geometry, then you can specify how to combine the values to compute the fill color.

FillBluesFromMean
FillBluesFromSum
../_images/show-map-examples-color-scheme-blue-mean.png ../_images/show-map-examples-color-scheme-blue-sum.png

Deploy geotable-based tool locally

If you are deploying your geotable-based tool on a local server, then you can take advantage of higher API rate limits for map tiles by specifying a Mapbox access token.

Set the MAPBOX_TOKEN environment variable before running the server. Here is the syntax in Linux:

$ export MAPBOX_TOKEN=YOUR-ACCESS-TOKEN
$ crosscompute serve