Skip to main content

Strict Text Template Parse (sttp) test codecov Documentation Status

Please see full documentation on readthedocs.

STTP allows you to parse text strictly (see why strict parsing below for a discussion on why parsing should be strict) but very easily, using a template which can be built by copying some sample output, marking the bits which change, and adding simple prefixes to the lines to indicate where they can recur multiple times.

Parsing is often a choice between doing something pragmatic, quick and dirty and getting stuff done... or spending a lot more time doing something better and more robust. STTP is about getting the best of both worlds, a super robust solution that is also quick and easy.

To give you an quick idea, take this made up input:

Num   Server               Uptime
1     wibble.domain.com    1d 5h
2     zap.domain.com       100d 1h
3     foobar.domain.com    3d 10h

You can parse this with this template:

m> Num   Server               Uptime
m*> {{ num = integer }}{{ ws }}{{ server = non_ws }}{{ ws }}{{ uptime = string }}

The result would be:

[
    {'num': 1, 'server': 'wibble.domain.com', 'uptime': '1d 5h'},
    {'num': 2, 'server': 'zap.domain.com',    'uptime': '100d 1h'},
    {'num': 3, 'server': 'foobar.domain.com', 'uptime': '3d 10h'},
]

You would do it like this:

parser = sttp.Parser(template = in_template)
out_struct = parser.parse(in_text)

Another quick example parsing the output of a ping command (such as ping -c3 dns.google):

PING dns.google (8.8.4.4) 56(84) bytes of data.
64 bytes from dns.google (8.8.4.4): icmp_seq=1 ttl=54 time=11.7 ms
64 bytes from dns.google (8.8.4.4): icmp_seq=2 ttl=54 time=12.5 ms
64 bytes from dns.google (8.8.4.4): icmp_seq=3 ttl=54 time=11.7 ms

--- dns.google ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 11.719/11.973/12.465/0.347 ms

This can be achieved with this template:

m> PING {{ target = hostname }} ({{ ipaddr = ipaddr }}) {{ integer }}({{ integer }}) bytes of data.
m*(replies)> {{ len = integer }} bytes from {{ target = hostname }} ({{ ipaddr = ipaddr }}): icmp_seq={{ seq = integer }} ttl={{ ttl = integer }} time={{ latency = number }} ms
m>
m> --- {{ target = hostname }} ping statistics ---
m> {{ stats.transmitted = integer }} packets transmitted, {{ stats.received = integer }} received, {{ stats.loss = integer }}% packet loss, time {{ stats.time = number }}ms
m> rtt min/avg/max/mdev = {{ stats.min = number }}/{{ stats.avg = number }}/{{ stats.max = number }}/{{ stats.mdev = number }} ms

The result would be:

{
    'ipaddr': '8.8.4.4',
    'target': 'dns.google',
    'replies': [
        {'len': 64, 'target': 'dns.google', 'ipaddr': '8.8.4.4', 'seq': 1, 'ttl': 54, 'latency': 11.7},
        {'len': 64, 'target': 'dns.google', 'ipaddr': '8.8.4.4', 'seq': 2, 'ttl': 54, 'latency': 12.5},
        {'len': 64, 'target': 'dns.google', 'ipaddr': '8.8.4.4', 'seq': 3, 'ttl': 54, 'latency': 11.7},
    ],
    'stats': {
        'transmitted': 3,
        'received': 3,
        'loss': 0,
        'time': 2002.0,
        'min': 11.719,
        'max': 12.465,
        'avg': 11.973,
        'mdev': 0.347,
    },
}

Why strict parsing?

To give you an quick idea of the parsing problems that can arise with the simplest of cases, take this made up input (the same input is used in the quick start):

Num   Server               Uptime
1     wibble.domain.com    1d 5h
2     zap.domain.com       100d 1h
3     foobar.domain.com    3d 10h

You can parse this simply like this:

>>> import re
>>> in_text = '''Num   Server               Uptime
... 1     wibble.domain.com    1d 5h
... 2     zap.domain.com       100d 1h
... 3     foobar.domain.com    3d 10h
... '''
>>> out_struct = [
...     entry.groupdict() for entry in
...     [re.match(r'(?P<num>\d+)\s+(?P<server>\S+)\s+(?P<uptime>\d+d \d+h)', line)
...         for line in in_text.split('\n')]
...     if entry is not None
... ]
>>> assert out_struct == [
...     {'num': '1', 'server': 'wibble.domain.com', 'uptime': '1d 5h'},
...     {'num': '2', 'server': 'zap.domain.com',    'uptime': '100d 1h'},
...     {'num': '3', 'server': 'foobar.domain.com', 'uptime': '3d 10h'}
... ], out_struct

This sort of parsing can be a quick and pragmatic way to get what you want but even this is not nearly as fast to write (and maintain) as using STTP, and there are pitfalls. For example if the output was completely unexpected, an error instead of the table for example, then the parse would still succeed! The result would be an empty array, but that might be perfectly legitimate if it wasn’t for the error. Or, what if there were table entries but with an error or warning as well. Of course you could check explicitly for errors you know about, or maybe you can recognise errors generally, but if there is an unexpected error or the error reporting format changes, you could be back to getting an empty array with an error check that doesn’t see an error any more. This sort of parsing is not strict, and it can be dangerous.

Naturally you can implement extremely strict parsing which will only tolerate exactly what you know of the text you are parsing and nothing else. Let’s see what that could look like in this example:

>>> in_text = '''Num   Server               Uptime
... 1     wibble.domain.com    1d 5h
... 2     zap.domain.com       100d 1h
... 3     foobar.domain.com    3d 10h
... '''
>>> lines = in_text.split('\n')
>>> if len(lines) == 0:
...     raise Exception('input is empty')
>>> header = lines.pop(0)
>>> if header != 'Num   Server               Uptime':
...     raise Exception('input line 1 was not recognised header: ' + header)
>>> out_struct = []
>>> while lines:
...     line = lines.pop(0)
...     match = re.match(r'(?P<num>\d+)\s+(?P<server>\S+)\s+(?P<uptime>\d+d \d+h)', line)
...
...     if match:
...         out_struct.append(match.groupdict())
...     elif line != '' or len(lines) != 0:
...         raise Exception('unexpected line parsing table entries: ' + line)
>>> assert out_struct == [
...     {'num': '1', 'server': 'wibble.domain.com', 'uptime': '1d 5h'},
...     {'num': '2', 'server': 'zap.domain.com',    'uptime': '100d 1h'},
...     {'num': '3', 'server': 'foobar.domain.com', 'uptime': '3d 10h'}
... ], out_struct

There’s nothing difficult about this, but WOW, 14 lines of code, it’s a long way from that pragmatic one liner, and it would take you a LOT longer to write it than the STTP template version, where the only interesting bit is the template:

m> Num   Server               Uptime
m*> {{ int num = integer }}{{ ws }}{{ server = non_ws }}{{ ws }}{{ uptime = string }}

Fail fast

In parsing terms this means “only accept what you know, handle it correctly and crash for any unknown”. That is a Fail fast approach. Fail fast advocates that if something unexpected happens it’s better to fail immediately and clearly with all the context of the failure intact, than try to carry on with possibly invalid results (and no way of knowing it), causing any number of side effects later, such as an exception not obviously related to the parse at all, or simply incorrect data, and maybe that data could be put in a database, and there’s probably no chance anyone will easily figure out why that bit of data is wrong this time next week…

Fail fast might mean that something crashes in production that wouldn’t have if you had less strict parsing, but isn’t that crash better than corrupting a database without knowing it? A crash and stack trace at the right time can often provide developers all they need to know to understand what went wrong, and with good monitoring, efficient agile toolchains and release processes, a new unit test could have been written, the bug fixed, and a new revision released to production in minutes!

Release files for sttp 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sttp 0.0.3
File Size Uploaded
sttp-0.0.3.tar.gz 18.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sttp 0.0.3
File Interpreter ABI Platform
sttp-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 37.5 kB

Release files / sttp-0.0.3.tar.gz

Download URL sttp-0.0.3.tar.gz
Size 18.3 kB
Tags Source
SHA-256 checksum
How to use checksums
3b367cc7438444b2c8b1dc7463bf382c24536f2ea44bf0dbc93eff6f155c35b3
BLAKE2b-256 checksum
How to use checksums
92e34b269fb9548c494e2b84eb96ddc85ff6b82b873dd878e090a6c7f45e2114
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.8.0 pkginfo/1.8.2 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/18.0.1 rfc3986/2.0.0 colorama/0.4.3 CPython/3.8.10

Release files / sttp-0.0.3-py3-none-any.whl

Download URL sttp-0.0.3-py3-none-any.whl
Size 19.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
283c42eb60d71bfbcf65a42ff9031278fcfae4cf4510a2d6c0ea5784c33e7f46
BLAKE2b-256 checksum
How to use checksums
996fa5c710f51bb5579f20c5d38823e1c077cdd1984c479c3c6163396929e35d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.8.0 pkginfo/1.8.2 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.63.0 importlib-metadata/4.11.2 keyring/18.0.1 rfc3986/2.0.0 colorama/0.4.3 CPython/3.8.10

Release history Release notifications | RSS feed

This release

0.0.3 This release

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page