libasterix


Namelibasterix JSON
Version 0.21.0 PyPI version JSON
download
home_pageNone
SummaryAsterix data processing library
upload_time2025-08-21 08:42:01
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords asterix eurocontrol radar
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Asterix data processing library for python

Features:

- asterix data parsing/decoding from bytes
- asterix data encoding/unparsing to bytes
- precise conversion functions for physical quantities
- support for many asterix categories and editions
- support for Reserved Expansion Fields (REF)
- support for Random Field Sequencing (RFS)
- support for categories with multiple UAPs, eg. cat001
- support for context dependent items, eg. I062/380/IAS
- support for strict or partial record parsing, to be used
  with so called blocking or non-blocking asterix categories
- support to encode zero, one or more records in a datablock
- pure python implementation
- type annotations for static type checking,
  including subitem access by name

## Example

Encoding and decoding asterix example.
This example also includes type annotations for static
type checking with `mypy`. In a simple untyped environment,
the type annotations and assertions could be skipped.

```python
#| file: example0.py
from typing import *
from binascii import hexlify, unhexlify
from dataclasses import dataclass

from asterix.base import *
import asterix.generated as gen

# Select particular asterix categories and editions
Cat034 = gen.Cat_034_1_29
Cat048 = gen.Cat_048_1_32

# Example messages for this application
class Token:
    pass

@dataclass
class NorthMarker(Token):
    pass

@dataclass
class SectorCrossing(Token):
    azimuth: float

@dataclass
class Plot(Token):
    rho: float
    theta: float
    ssr: str

# example message to be encoded
tx_message = [
    NorthMarker(),
    SectorCrossing(0.0),
    Plot(rho=10.0, theta=45.0, ssr='7777'),
    SectorCrossing(45.0),
]
print('sending message:', tx_message)

# encode token to datablock
def encode(token: Token) -> bytes:
    if isinstance(token, NorthMarker):
        rec034 = Cat034.cv_record.create({
            '000': 1, # North marker message
            '010': (('SAC', 1), ('SIC', 2)),
        })
        datablock034 = Cat034.create([rec034])
        return datablock034.unparse().to_bytes()
    if isinstance(token, SectorCrossing):
        rec034 = Cat034.cv_record.create({
            '000': 2, # Sector crossing message
            '010': (('SAC', 1), ('SIC', 2)),
            '020': ((token.azimuth, "°")),
        })
        datablock034 = Cat034.create([rec034])
        return datablock034.unparse().to_bytes()
    if isinstance(token, Plot):
        rec048 = Cat048.cv_record.create({
            '010': (('SAC', 1), ('SIC', 2)),
            '040': (('RHO', (token.rho, "NM")), ('THETA', (token.theta, "°"))),
            '070': (0, 0, 0, 0, ('MODE3A', token.ssr)),
        })
        datablock048= Cat048.create([rec048])
        return datablock048.unparse().to_bytes()
    raise Exception('unexpected token', token)

datablocks = [encode(token) for token in tx_message]
tx = b''.join(datablocks)
print('bytes on the wire:', hexlify(tx))

assert hexlify(tx) == \
    b'220007c0010201220008d00102020030000c9801020a0020000fff220008d001020220'

# decode bytes to message list
def decode(rx_bytes: bytes) -> List[Token]:
    message: List[Token] = []

    raw_datablocks = RawDatablock.parse(Bits.from_bytes(tx))
    assert not isinstance(raw_datablocks, ValueError)
    for db in raw_datablocks:
        cat = db.get_category()
        if cat == 34:
            result034 = Cat034.cv_uap.parse(db.get_raw_records())
            assert not isinstance(result034, ValueError)
            for rec034 in result034:
                i000 = rec034.get_item('000')
                assert i000 is not None
                val = i000.as_uint()
                if val == 1:
                    message.append(NorthMarker())
                elif val == 2:
                    i020 = rec034.get_item('020')
                    assert i020 is not None
                    azimuth = i020.variation.content.as_quantity("°")
                    message.append(SectorCrossing(azimuth = azimuth))
                else:
                    pass
        elif cat == 48:
            result048 = Cat048.cv_uap.parse(db.get_raw_records())
            assert not isinstance(result048, ValueError)
            for rec048 in result048:
                i040 = rec048.get_item('040')
                i070 = rec048.get_item('070')
                assert i040 is not None
                assert i070 is not None
                rho = i040.variation.get_item('RHO').variation.content.as_quantity("NM")
                theta = i040.variation.get_item('THETA').variation.content.as_quantity("°")
                ssr = i070.variation.get_item('MODE3A').variation.content.as_string()
                message.append(Plot(rho = rho, theta = theta, ssr = ssr))
        else:
            pass
    return message

rx = tx
rx_message = decode(rx)

# expect the same message
print('received message:', rx_message)
assert rx_message == tx_message
```

## Installation

Use any of the following methods:

### Method 1

Install from python package index <https://pypi.org/project/libasterix/>:

``` bash
pip install libasterix
```

### Method 2

Install from github:

``` bash
# (default branch)
pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git#egg=libasterix&subdirectory=libs/python"

# ('devel' branch)
pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git@devel#egg=libasterix&subdirectory=libs/python"
```

### Method 3

Manually copy library files from [repository](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix).

Download and copy files either alongside your project sources or
to some location where `python` can find it.

```bash
# check default python path
python3 -c "import sys; print('\n'.join(sys.path))"
```

## Tutorial

Check library installation.

```bash
python3 -c "import asterix.base as base; print(base.AstSpec)"
python3 -c "import asterix.generated as gen; print(gen.manifest['CATS'].keys())"
```

### Import

This tutorial assumes importing complete `asterix` module into the current
namespace. In practice however only the required objects could be imported
or the module might be imported to a dedicated namespace.

```python
from asterix.base import *
from asterix.generated import *
```

### Error handling

Some operation (eg. parsing) can fail on unexpected input. In such case,
to indicate an error, this library will not raise an exception, but will
return `ValueError('problem description')` instead.

With this approach, a user can handle errors in a type safe way, for example:

```python
def parse_datablocks(s: bytes) -> List[RawDatablock]:
    dbs = RawDatablock.parse(Bits.from_bytes(s))
    if isinstance(dbs, ValueError):
        return [] # or raise exception, or ...
    return dbs
```

For clarity, the error handling part is skipped in some parts of this tutorial.

### Immutable objects

All operation on asterix objects are *immutable*.

For example:

```python
#| file: example-immutable.py
from asterix.generated import *

Spec = Cat_002_1_1

# create empty record
rec0 = Spec.cv_record.create({})

# this operation does nothing (result is not stored)
rec0.set_item('000', 1)
assert rec0.get_item('000') is None

# store result to 'rec1'
rec1 = rec0.set_item('000', 1)
assert rec1.get_item('000') is not None

# del_item, store result to 'rec1a'
rec1a = rec1.del_item('000')
assert rec1a.get_item('000') is None

# use multiple updates in sequence
rec2a = rec0.set_item('000', 1).set_item('010', (('SAC', 1), ('SIC', 2)))
rec2b = Spec.cv_record.create({'000': 1, '010': (('SAC', 1), ('SIC', 2))})
assert rec2a.unparse() == rec2b.unparse()

# mutation can be simulated by replacing old object with the new one
# (using the same variable name)
rec0 = rec0.set_item('000', 1)
assert rec0.get_item('000') is not None
```

### Miscellaneous project and source code remarks

- `cv_{name}` stands for *class variable*, to avoid name clash with
  *instance variable* with the same name (which are without prefix).
- `RuleContent` and `RuleVariation` are necessary to cope with some
  small number of irregular cases with asterix definitions
  (that is: context dependent definitions).
- `NonSpare` is (as name suggests) an item with some defined content.
  It is a separate class from `Item` and `Spare`, to reuse definition
  in different contexts, for example `Compound` subitems are `NonSpare`.

### Asterix specifications as python classes

Asterix specifications hierarchy is reflected in python classes.
For example: Category `062` contains item `010`, which in turn contains
subitems `SAC` and `SIC`.

There is a `spec` class method which follows this structure deeper to the
hierarchy. For example:

```python
#| file: example-spec.py
from binascii import unhexlify
from asterix.generated import *

# Use cat062, edition 1.20
Spec = Cat_062_1_20

print(Spec)
print('category number', Spec.cv_category)
print('edition', Spec.cv_edition)

# Extract deeper specs
Uap = Spec.cv_uap
Record = Uap.cv_record
I010 = Record.spec('010')
SAC = I010.cv_rule.cv_variation.spec('SAC')
print(SAC)

# Use more direct way to extract subspec
SIC = Spec.cv_uap.cv_record.spec('010').cv_rule.cv_variation.spec('SIC')
print(SIC)

# SAC and SIC subitems are both 8-bits long raw values (same structure),
# so thay both map to the same class.
assert (SAC==SIC)

# With this specification it is possible to perform low level
# asterix operations, for example to parse a single subitem
sample_bits = Bits.from_bytes(unhexlify(b'ff0102fe'))
print(sample_bits)
result = SIC.parse(sample_bits)
assert not isinstance(result, ValueError)
sic_object, remaining_bits = result
print(remaining_bits)
print(sic_object)
print(sic_object.unparse())
print(sic_object.as_uint())

# Similarly, it is possible to extract other parts, for example
# extended subitems
print(Record.spec('080').cv_rule.cv_variation.spec('MON').cv_rule)
# compound subitems
print(Record.spec('110').cv_rule.cv_variation.spec('SUM').cv_rule)
```

See [base.py](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix/base.py)
and [generated.py](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix/generated.py)
for details.

### Datagram

Datagram is a raw binary data as received for example from UDP socket.
This is represented with `bytes` data type in python.

### Raw Datablock

Raw datablock is asterix datablock in the form `cat|length|data` with the
correct byte size. A datagram can contain multiple datablocks.

This is represented in python with `class RawDatablock`.

In some cases it might be sufficient to work with raw datablocks, for example
in the case of asterix category filtering. In this case, it is not required
to fully parse asterix records.

**Example**: Category filter, drop datablocks if category == 1

```python
#| file: example1.py
from binascii import hexlify, unhexlify
from asterix.base import *

def receive_from_udp() -> bytes: # UDP rx text function
    return unhexlify(''.join([
        '01000401', # cat1 datablock
        '02000402', # cat2 datablock
        ]))

def send_to_udp(s: bytes) -> None: # UDP tx test function
    print(hexlify(s))

input_data = Bits.from_bytes(receive_from_udp())
raw_datablocks = RawDatablock.parse(input_data) # can fail on wrong input
assert not isinstance(raw_datablocks, ValueError)
valid_datablocks = [db.unparse().to_bytes() \
                    for db in raw_datablocks if db.get_category() != 1]
output_data = b''.join(valid_datablocks)
send_to_udp(output_data)
```

### Datablock, Record

Datablock (represented as `class Datablock`) is a higher level, where we
have a guarantee that all containing records are semantically correct
(asterix is fully parsed or correctly constructed).

Datablock/Record is required to work with asterix items and subitems.

**Example**: Create 2 records and combine them to a single datablock

```python
#| file: example2.py
from binascii import hexlify
from asterix.generated import *

Spec = Cat_002_1_1 # use cat002, edition 1.1

rec1 = Spec.cv_record.create({
    '000': 1,
    '010': (('SAC', 1), ('SIC', 2)),
    })

rec2 = Spec.cv_record.create({
    '000': 2,
    '010': (('SAC', 1), ('SIC', 2)),
    })

db = Spec.create([rec1, rec2])
s = db.unparse().to_bytes() # ready to send over the network
print(hexlify(s))
```

**Example**: Parse datagram (from the example above) and extract message type
from each record

```python
#| file: example3.py
from binascii import unhexlify
from asterix.base import *
from asterix.generated import *

Spec = Cat_002_1_1 # use cat002, edition 1.1

s = unhexlify(b'02000bc0010201c0010202') # ... use data from the example above
raw_datablocks = RawDatablock.parse(Bits.from_bytes(s)) # can fail on wrong input
assert not isinstance(raw_datablocks, ValueError)
for db in raw_datablocks:
    records = Spec.cv_uap.parse(db.get_raw_records()) # can fail on wrong input
    assert not isinstance(records, ValueError)
    for record in records:
        i000 = record.get_item('000') # returns None if the item is not present
        assert i000 is not None
        raw_value = i000.as_uint()
        description = i000.variation.content.table_value()
        print('{}: {}'.format(raw_value, description))
```

**Example**: Asterix filter, rewrite SAC/SIC code with random values.

```python
#| file: example4.py
import time
import random
from asterix.base import *
from asterix.generated import *

# categories/editions of interest
Specs = {
    48: Cat_048_1_31,
    62: Cat_062_1_19,
    63: Cat_063_1_6,
    # ...
    }

def process_record(sac, sic, rec):
    """Process single record."""
    return rec.set_item('010', (('SAC', sac), ('SIC', sic)))

def process_datablock(sac, sic, db):
    """Process single raw datablock."""
    cat = db.get_category()
    Spec = Specs.get(cat)
    if Spec is None:
        return db
    # second level of parsing (records are valid)
    records = Spec.cv_uap.parse(db.get_raw_records())
    new_records = [process_record(sac, sic, rec) for rec in records]
    return Spec.create(new_records)

def rewrite_sac_sic(sac : int, sic : int, s : bytes) -> bytes:
    """Process datagram."""
    # first level of parsing (datablocks are valid)
    raw_datablocks = RawDatablock.parse(Bits.from_bytes(s))
    result = [process_datablock(sac, sic, db) for db in raw_datablocks]
    output = b''.join([db.unparse().to_bytes() for db in result])
    return output

def rx_bytes_from_the_network():
    """Dummy rx function (generate valid asterix datagram)."""
    time.sleep(1)
    Spec = Cat_048_1_31
    rec = Spec.cv_record.create({'010': 0, '040': 0})
    db1 = Spec.create([rec, rec]).unparse().to_bytes()
    db2 = Spec.create([rec, rec]).unparse().to_bytes()
    return b''.join([db1, db2])

def tx_bytes_to_the_network(s_output):
    """Dummy tx function."""
    print(hexlify(s_output))

# main processing loop
cnt = 0
while True:
    s_input = rx_bytes_from_the_network()
    new_sac = random.randint(0,127)
    new_sic = random.randint(128,255)
    try:
        s_output = rewrite_sac_sic(new_sac, new_sic, s_input)
        tx_bytes_to_the_network(s_output)
    except Exception as e:
        print('Asterix exception: ', e)
    # only run a few iterations for test
    cnt += 1
    if cnt > 3:
        break
```

#### Spare items

Some bits are defined as *Spare*, which are normally set to `0`.
With this library:

- A user is able set spare bits to any value, including abusing spare bits
  to contain non-zero value.
- When parsing data, tolerate spare bits to contain any value. It is up
  to the application to check the spare bits if desired.

Multiple spare bit groups can be defined on a single item.
`get_spares` method returns the actual values of all spare bit groups.

**Example**

```python
#| file: example-spare.py
from asterix.generated import *

# I062/120 contain single group of spare bits
Spec = Cat_062_1_20

# create regular record with spare bits set to '0'
rec1 = Spec.cv_record.create({
    '120': (0, ('MODE2', 0x1234)),
})
i120a = rec1.get_item('120')
assert i120a is not None
spares1 = i120a.variation.get_spares()
assert spares1 == [0]

# create record, abuse spare bits, set to '0xf'
rec2 = Spec.cv_record.create({
    '120': (0xf, ('MODE2', 0x1234)),
})
i120b = rec2.get_item('120')
assert i120b is not None
spares2 = i120b.variation.get_spares()
assert spares2 == [0xf]
```

#### Reserved expansion fields

This library supports working with expansion fields. From the `Record`
prespective, the `RE` item contains raw bytes, without any structure,
similar to how a datablock contains raw bytes without a structure. Parsing
raw datablocks and parsing records are 2 separate steps. In the same
vain, parsing `RE` out of the record would be a third step. Once parsed,
the `RE` item  gets it's structure, and it's possible to access it's subitems,
similar to a regular record/subitem situation.

When constructing a record with the `RE` item, a user must first
construct the `RE` item itself, unparse it to bytes and insert bytes
as a value of the `RE` item of a record.

A reason for this separate stage approach is that a category and expansion
specification can remain separate to one another. In addition, a user has
a possiblity to explicitly select both editions individually.

This example demonstrates required steps for constructing and parsing:

```python
#| file: example5.py
from asterix.generated import *

Spec = Cat_062_1_20
Ref  = Ref_062_1_3

# create 'RE' subitem
ref = Ref.cv_expansion.create({
    'CST': [0],
    'CSN': [1,2],
    'V3': {
        'PS3': 0,
        },
    })

# create record, insert 'RE' subitem as bytes
rec = Spec.cv_record.create({
    '010': (('SAC', 1), ('SIC', 2)),
    'RE': ref.unparse().to_bytes(),
    })

db = Spec.create([rec])
s = db.unparse()
assert s.to_bytes().hex() == \
    '3e001b8101010104010211c8010000000000020000010000028000'

# first stage, parse to the record
raw_datablocks = RawDatablock.parse(s)
assert not isinstance(raw_datablocks, ValueError)
assert len(raw_datablocks) == 1 # expecting 1 datablock
result1 = Spec.cv_uap.parse(raw_datablocks[0].get_raw_records())
assert not isinstance(result1, ValueError)
assert len(result1) == 1 # expecting one record

# get 'RE' subitem,
re_subitem = result1[0].get_item('RE')
assert re_subitem is not None
re_bytes = re_subitem.variation.get_bytes()

# second stage: parse 'RE' structure
result2 = Ref.cv_expansion.parse(Bits.from_bytes(re_bytes))
assert not isinstance(result2, ValueError)
ref_readback, remaining = result2
assert remaining.null()
# expecting the same 'ref' as the original
assert ref.unparse() == ref_readback.unparse()
# we have a structure back and we can extract the values
result3 = ref_readback.get_item('CSN')
assert result3 is not None
lst = result3.variation.get_list()
assert len(lst) == 2
assert lst[0].as_uint() == 1
assert lst[1].as_uint() == 2
```

#### Dependent specifications

In some rare cases, asterix definitions depend on a value of some other
item(s). In such cases, the asterix processing is more involved. This
dependency manifests itself in two ways:

- **content dependency**, where a content (interpretation of bits) of some
  item depends on the value of some other item(s). For example:
  `I062/380/IAS/IAS`, the structure is always 15 bits long,
  but the interpretation of bits could be either speed in `NM/s` or `Mach`,
  with different scaling factors, depending on the values of a sibling item.
- **variation dependency**, where not only the content, but also a complete
  item stucture depends on some other item(s). For example, the structure
  of item `I004/120/CC/CPC` depends on 2 other item values.

This library can handle all structure cases, however it does not automatically
correlate to the values of items that a structure depends on. When creating
records, it is a user responsibility to properly set "other item values" for
a record to be valid.
Similarly, after a record is parsed, a user shall cast a default structure to a
target structure, depending on the other items values. Whenever there is a
dependency, there is also a statically known *default* structure, which is
used during automatic record parsing.

##### Handling **content dependency**

This example demonstrates how to work with **content dependency**,
such as `I062/380/IAS`.

```python
#| file: dep-content.py
from asterix.base import *
from asterix.generated import *

Spec = Cat_062_1_20 # Cat 062, edition 1.20

# create records by different methods

# set raw value
rec0 = Spec.cv_record.create({
    '380': {
        'IAS': (
            ('IM', 0),  # set IM to 0
            ('IAS', 1)  # set IAS to raw value 1 (no unit conversion)
        ) },
    })

# set raw value using default case
rec1 = Spec.cv_record.create({
    '380': {
        'IAS': (
            ('IM', 0),  # set IM to 0
            ('IAS',
                (None,  # use default raw case
                 1))    # set IAS to raw value 1 (same as above)
        ) },
    })

# set IAS speed (NM/s)
rec2 = Spec.cv_record.create({
    '380': {
        'IAS': (
            ('IM', 0), # airspeed = IAS
            ('IAS',
                ((0,), # use case with index 0 (IAS)
                 (1.2, 'NM/s'))), # set IAS to 1.2 NM/s
        ) },
    })

# set Mach speed
rec3 = Spec.cv_record.create({
    '380': {
        'IAS': (
            ('IM', 1), # airspeed = Mach
            ('IAS',
                ((1,), # use case with index 1 (Mach)
                 (0.8, 'Mach'))), # set speed to 0.8 Mach
        ) },
    })

db = Spec.create([rec0, rec1, rec2, rec3])
expected_output = b'3e0017011010000101101000010110104ccd0110108320'
assert hexlify(db.unparse().to_bytes()) == expected_output

# parse and interpret data from the example above
input_bytes = unhexlify(expected_output)
raw_datablocks = RawDatablock.parse(Bits.from_bytes(input_bytes))
assert not isinstance(raw_datablocks, ValueError)
for db in raw_datablocks:
    assert db.get_category() == 62
    result = Spec.cv_uap.parse(db.get_raw_records())
    assert not isinstance(result, ValueError)
    for (cnt, rec) in enumerate(result):
        i380 = rec.get_item('380')
        assert i380 is not None
        item_IAS1 = i380.variation.get_item('IAS')
        assert item_IAS1 is not None
        item_IM = item_IAS1.variation.get_item('IM')
        item_IAS2 = item_IAS1.variation.get_item('IAS')
        assert item_IM is not None
        assert item_IAS2 is not None
        match item_IM.as_uint(): # check value of I062/380/IAS/IM and convert
            case 0: # this is IAS, convert to 'NM/s', use case with index (0,)
                value = ('NM/s', item_IAS2.variation.rule.content((0,)).as_quantity('NM/s'))
            case 1: # this is Mach, convert to 'Mach', use case with index (1,)
                value = ('Mach', item_IAS2.variation.rule.content((1,)).as_quantity('Mach'))
            case _:
                raise Exception('unexpected value')
        print('--- record', cnt, '---')
        print('I062/380/IAS/IM raw value:', item_IM.as_uint())
        print('I062/380/IAS/IAS raw value:', item_IAS2.as_uint())
        print('converted value', value)
```

##### Handling **variation dependency**

This example demonstrates how to work with **variation dependency**,
such as `I004/120/CC/CPC`.

```python
#| file: dep-variation.py
from asterix.base import *
from asterix.generated import *

Spec = Cat_004_1_13 # Cat 004, edition 1.13

# Item 'I004/120/CC/CPC' depends on I004/000 and I004/120/CC/TID values
# Default case is: element3, raw, but there are many other cases.
# See asterix specification for details.
# This example handles the following cases:
# case (5, 1): element 3, table
# case (9, 2): group (('RAS', element1, table), spare 2)

# case (0, 0) - invalid combination
rec0 = Spec.cv_record.create({
    '000': 0, # invalid value
    '120': {
        'CC': (
            ('TID', 0),
            ('CPC', 0), # set to raw value 0
            ('CS', 0)
            )
        }
    })

# case (5, 1)
rec1 = Spec.cv_record.create({
    '000': 5, # Area Proximity Warning (APW)
    '120': {
        'CC': (
            ('TID', 1),
            ('CPC', 0), # structure is 'raw', set to 0
            ('CS', 0)
            )
        }
    })

# case (9, 2)
# get variation structure of case (9, 2)
Var = Spec.cv_record.spec('120').cv_rule.cv_variation.spec('CC').\
    cv_rule.cv_variation.spec('CPC').spec((9,2))
# and create object of that structure ('RAS' + spare item)
obj = Var.create(( ('RAS', 1), 0))  # RAS = Stage Two Alert

# insert object into the record (as uint)
rec2 = Spec.cv_record.create({
    '000': 9, # RIMCAS Arrival / Landing Monitor (ALM)
    '120': {
        'CC': (
            ('TID', 2),
            ('CPC', obj.as_uint()),
            ('CS', 0)
            )
        }
    })

db = Spec.create([rec0, rec1, rec2])
expected_output = b'040012412000400041200540104120094028'
assert hexlify(db.unparse().to_bytes()) == expected_output

# parse and interpret data from the example above
input_bytes = unhexlify(expected_output)
raw_datablocks = RawDatablock.parse(Bits.from_bytes(input_bytes))
assert not isinstance(raw_datablocks, ValueError)
for db in raw_datablocks:
    assert db.get_category() == 4
    result = Spec.cv_uap.parse(db.get_raw_records())
    assert not isinstance(result, ValueError)
    for (cnt, rec) in enumerate(result):
        print('--- record', cnt, '---')
        i000 = rec.get_item('000')
        i120 = rec.get_item('120')
        assert i000 is not None and i120 is not None
        item_CC = i120.variation.get_item('CC')
        assert item_CC is not None
        item_TID = item_CC.variation.get_item('TID').as_uint()
        item_CPC = item_CC.variation.get_item('CPC')
        item_CS  = item_CC.variation.get_item('CS').as_uint()
        index = (i000.as_uint(), item_TID)

        try:
            var_CPC = item_CPC.variation(index)
        except Exception:
            var_CPC = None
        assert not isinstance(var_CPC, ValueError)

        match index:
            case (5, 1):
                x = var_CPC.as_uint()
                value = ('case 5,1', 'raw', x)
            case (9, 2):
                item_ras = var_CPC.get_item('RAS')
                spares = var_CPC.get_spares()
                value = ('case 9,2', 'RAS', item_ras.as_uint(), spares)
            case _:
                value = None
        print(value)
```

#### Multiple UAP-s

Make sure to use appropriate UAP name, together with a correct UAP selector
value, for example for CAT001:

- `['020', 'TYP'] = 0` for `plot`
- `['020', 'TYP'] = 1` for `track`

```python
#| file: example6.py
from asterix.base import *
from asterix.generated import *

Cat1 = Cat_001_1_4

rec01_plot = Cat1.cv_uap.spec('plot').create({
    '010': 0x0102,
    '020': ((('TYP',0),0,0,0,0,0,None),),
    '040': 0x01020304
})

rec01_track = Cat1.cv_uap.spec('track').create({
    '010': 0x0102,
    '020': ((('TYP',1),0,0,0,0,0,None),),
    '040': 0x01020304,
    })

rec01_invalid = Cat1.cv_uap.spec('plot').create({
    '010': 0x0102,
    '020': ((('TYP',1),0,0,0,0,0,None),),
    '040': 0x01020304
})

print(Cat1.create([rec01_plot]).unparse().to_bytes().hex())
print(Cat1.create([rec01_track]).unparse().to_bytes().hex())
print(Cat1.create([rec01_invalid]).unparse().to_bytes().hex())
```

### RFS handling

This library supports RFS mechanism for categories that include RFS
indicator(s). For such cases, it is possible to sequence subitems in
any order. Once such record is created or parsed, a user can extract
subitems using `get_rfs_item` method. The result in this case is
a list, since the item can be present in the record multiple times.
An empty list indicates that no such item is present in the RFS.

**Example**

```python
#| file: example-rfs.py
from binascii import hexlify
from asterix.generated import *

# cat008 contains RFS indicator, so we are able to add RFS items
Spec = Cat_008_1_3

rec1 = Spec.cv_record.create({
    '000': 1,                                   # add item '000' (regular)
    '010': (('SAC', 1), ('SIC', 2)),            # add item '010' (regular)
    },
    [
        ('010', (('SAC', 3), ('SIC', 4))),      # add item '010' as RFS
        ('010', (('SAC', 4), ('SIC', 5))),      # add another item '010' as RFS
    ]
    )

# extract regular item 010
i010_regular = rec1.get_item('010')
print(i010_regular)

# extract RFS items 010, expecting 2 such items
i010_rfs = rec1.get_rfs_item('010')
assert len(i010_rfs) == 2
for i in i010_rfs:
    print(i)

# but item '000' is not present in RFS
assert len(rec1.get_rfs_item('000')) == 0
```

### Strict and partial record parsing modes

This library supports parsing records strictly or partially.

In a strict mode, we want to make sure that all data is parsed exactly
as specified in the particular category/edition schema. The record parsing
fails if the FSPEC parsing fails or if any subsequent item parsing fails.

I a partial mode, we don't require exact parsing match. If we know where
in a bytestring a record starts, we can try to parse *some* information out of
the data stream, even in the case if the editions of the transmitter and the
receiver do not match exactly. In particular: if the transmitter sends some
additional items, unknown to the receiver. In that case, the receiver can still
parse up to some point in a datablock.

Partial record parsing means to parse the FSPEC (which might fail) followed
by parsing subitems up to the point until items parsing is successful. The
record parsing only fails if the FSPEC parsing itself fails.

This is useful in situations where a datablock contains only one record
(known as *non-blocking* in Asterix Maintenance Group vocabulary) or if
we are interested only in the first record (even if there are more). The idea
is to regain some forward compatibility on the receiver side, such that the
receiver does not need to upgrade edition immediately as the transmitter
upgrades or even before that. Whether this is safe or not, depends on the
application and the exact differences between transmitter and receiver
asterix editions.

The following parsing methods exist:

- `UapClass.parse(s: Bits) -> ValueError or List[Record]`
- `RecordClass.parse(pm: ParsingMode, s: Bits) -> ValueError or Record + remaining`

`ParsingMode` is an `Enum` with the following options:

- `StrictParsing`
- `PartialParsing`

Calling `parse` on some `Uap` class returns **list of records** on success.
This method always uses *strict* parsing and it makes sure it consumes all
input data.

Calling `parse` on some `Record` class returns that record instance and
the remaining bytes. A method also requires parsing mode to be specified.

Both methods can fail on invalid input data (return `ValueError`).

This example demonstrates various parsing modes:

```python
Spec = Cat_NNN_E_E # some category/edition spec

s: Bits = db.get_raw_records() # some input bits to be parsed

# strictly parse records
# 'parse' is called on 'Uap' class
# successful result is a List of Records
result1 = Spec.cv_uap.parse(s)
if not isinstance(result1, ValueError):
    for r1 in result1:
        print(r1)

# strictly parse a single record
# 'parse' is called on 'Record' class
# successful result is (Record + remaining bytes)
result2 = Spec.cv_record.parse(ParsingMode.StrictParsing, s)
if not isinstance(result2, ValueError):
    r2, remaining = result2

# partially parse a single record
# successful result is (Record + remaining bytes),
result3 = Spec.cv_record.parse(ParsingMode.PartialParsing, s)
if not isinstance(result3, ValueError):
    r3, remaining = result3
```

### Library manifest

This library defines a `manifest` structure in the form:

```python
manifest = {
    'CATS': {
        1: [
            Cat_001_1_2,
            Cat_001_1_3,
            Cat_001_1_4,
        ],
        2: [
            Cat_002_1_0,
            Cat_002_1_1,
        ],
...
```

This structure can be used to extract *latest* editions for each defined
category, for example:

```python
#| file: example7.py
from asterix.generated import *

Specs = {cat: manifest['CATS'][cat][-1] for cat in manifest['CATS']}

for spec in Specs.values():
    print(spec.cv_category, spec.cv_edition)
```

Alternatively, a prefered way is to be explicit about each edition,
for example:

```python
#| file: example8.py
from asterix.generated import *

Specs = {
    48: Cat_048_1_31,
    62: Cat_062_1_19,
    63: Cat_063_1_6,
    # ...
    }
```

### Generic asterix processing

*Generic processing* in this context means working with asterix data where
the subitem names and types are determined at runtime. That is: the explicit
subitem names are never mentioned in the application source code.

This is in contrast to *application specific processing*, where we are
explicit about subitems, for example ["010", "SAC"].

**Example**: Show raw content of all toplevel items of each record

```python
#| file: example9.py
from binascii import unhexlify
from asterix.generated import *

Specs = {
    48: Cat_048_1_31,
    62: Cat_062_1_19,
    63: Cat_063_1_6,
    # ...
}

# some test input bytes
s = unhexlify(''.join([
    '3e00a5254327d835a95a0d0a2baf256af940e8a8d0caa1a594e1e525f2e32bc0448b',
    '0e34c0b6211b5847038319d1b88d714b990a6e061589a414209d2e1d00ba5602248e',
    '64092c2a0410138b2c030621c2043080fe06182ee40d2fa51078192cce70e9af5435',
    'aeb2e3c74efc7107052ce9a0a721290cb5b2b566137911b5315fa412250031b95579',
    '03ed2ef47142ed8a79165c82fb803c0e38c7f7d641c1a4a77740960737']))

def handle_nonspare(cat, name, nsp):
    print('cat{}, item {}, {}'.format(cat, name, nsp.unparse()))
    # depending on the application, we might want to display
    # deep subitems, which is possible by examining 'nsp' object

for db in RawDatablock.parse(Bits.from_bytes(s)):
    cat = db.get_category()
    Spec = Specs.get(cat)
    if Spec is None:
        print('unsupported category', cat)
        continue
    for record in Spec.cv_uap.parse(db.get_raw_records()):
        for (name, nsp) in record.items_regular.items():
            handle_nonspare(cat, name, nsp)
```

**Example**: Generate dummy single record datablock with all fixed items set to zero

```python
#| file: example10.py
from binascii import hexlify
from asterix.generated import *

# we could even randomly select a category/edition from the 'manifest',
# but for simplicity just use a particular spec
Spec = Cat_062_1_20

rec = Spec.cv_record.create({})
all_items = Spec.cv_record.cv_items_dict
for name in all_items:
    if name is None:
        continue
    nsp = all_items[name]
    var = nsp.cv_rule.cv_variation
    if issubclass(var, Element):
        rec = rec.set_item(name, 0)
    elif issubclass(var, Group):
        rec = rec.set_item(name, 0)
    elif issubclass(var, Extended):
        pass # skip for this test
    elif issubclass(var, Repetitive):
        pass # skip for this test
    elif issubclass(var, Explicit):
        pass # skip for this test
    elif issubclass(var, Compound):
        pass # skip for this test
    else:
        raise Exception('unexpected subclass')

s = Spec.create([rec]).unparse().to_bytes()
print(hexlify(s))
```

## Using `mypy` static code checker

**Note**: Tested with `mypy` version `1.9.0`.

[mypy](https://www.mypy-lang.org/) is a static type checker for Python.
It is recommended to use the tool on asterix application code, to identify
some problems which would otherwise result in runtime errors.

Consider the following test program (`test.py`):

```python
from asterix.generated import *

Spec = Cat_008_1_3
rec = Spec.cv_record.create({'010': (('SA',1), ('SIC',2))})
i010 = rec.get_item('010')
print(i010.variation.get_item('SA').as_uint())
```

The program contains the following bugs:
- Misspelled item name, `SA` instead of `SAC`, on lines 4 and 5
- `get_item('010')` result is not checked if the item
  is actually present, which might result in runtime error

```
$ python test.py
... results in runtime error (wrong item name)
$ pip install mypy
$ mypy test.py
... detects all problems, without actually running the program
Found 3 errors in 1 file (checked 1 source file)
```

Correct version of this program is:

```python
#| file: example11.py
from asterix.generated import *

Spec = Cat_008_1_3
rec = Spec.cv_record.create({'010': (('SAC',1), ('SIC',2))})
i010 = rec.get_item('010')
if i010 is not None:
    print(i010.variation.get_item('SAC').as_uint())
```

```
$ mypy test.py
Success: no issues found in 1 source file
$ python test.py
1
```

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "libasterix",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "asterix, eurocontrol, radar",
    "author": null,
    "author_email": "Zoran Bo\u0161njak <zoran.bosnjak@via.si>",
    "download_url": "https://files.pythonhosted.org/packages/f7/a2/6fc55c2a1fce50261f740cf07fe37ff19d95554723694692e42e09d88022/libasterix-0.21.0.tar.gz",
    "platform": null,
    "description": "# Asterix data processing library for python\n\nFeatures:\n\n- asterix data parsing/decoding from bytes\n- asterix data encoding/unparsing to bytes\n- precise conversion functions for physical quantities\n- support for many asterix categories and editions\n- support for Reserved Expansion Fields (REF)\n- support for Random Field Sequencing (RFS)\n- support for categories with multiple UAPs, eg. cat001\n- support for context dependent items, eg. I062/380/IAS\n- support for strict or partial record parsing, to be used\n  with so called blocking or non-blocking asterix categories\n- support to encode zero, one or more records in a datablock\n- pure python implementation\n- type annotations for static type checking,\n  including subitem access by name\n\n## Example\n\nEncoding and decoding asterix example.\nThis example also includes type annotations for static\ntype checking with `mypy`. In a simple untyped environment,\nthe type annotations and assertions could be skipped.\n\n```python\n#| file: example0.py\nfrom typing import *\nfrom binascii import hexlify, unhexlify\nfrom dataclasses import dataclass\n\nfrom asterix.base import *\nimport asterix.generated as gen\n\n# Select particular asterix categories and editions\nCat034 = gen.Cat_034_1_29\nCat048 = gen.Cat_048_1_32\n\n# Example messages for this application\nclass Token:\n    pass\n\n@dataclass\nclass NorthMarker(Token):\n    pass\n\n@dataclass\nclass SectorCrossing(Token):\n    azimuth: float\n\n@dataclass\nclass Plot(Token):\n    rho: float\n    theta: float\n    ssr: str\n\n# example message to be encoded\ntx_message = [\n    NorthMarker(),\n    SectorCrossing(0.0),\n    Plot(rho=10.0, theta=45.0, ssr='7777'),\n    SectorCrossing(45.0),\n]\nprint('sending message:', tx_message)\n\n# encode token to datablock\ndef encode(token: Token) -> bytes:\n    if isinstance(token, NorthMarker):\n        rec034 = Cat034.cv_record.create({\n            '000': 1, # North marker message\n            '010': (('SAC', 1), ('SIC', 2)),\n        })\n        datablock034 = Cat034.create([rec034])\n        return datablock034.unparse().to_bytes()\n    if isinstance(token, SectorCrossing):\n        rec034 = Cat034.cv_record.create({\n            '000': 2, # Sector crossing message\n            '010': (('SAC', 1), ('SIC', 2)),\n            '020': ((token.azimuth, \"\u00b0\")),\n        })\n        datablock034 = Cat034.create([rec034])\n        return datablock034.unparse().to_bytes()\n    if isinstance(token, Plot):\n        rec048 = Cat048.cv_record.create({\n            '010': (('SAC', 1), ('SIC', 2)),\n            '040': (('RHO', (token.rho, \"NM\")), ('THETA', (token.theta, \"\u00b0\"))),\n            '070': (0, 0, 0, 0, ('MODE3A', token.ssr)),\n        })\n        datablock048= Cat048.create([rec048])\n        return datablock048.unparse().to_bytes()\n    raise Exception('unexpected token', token)\n\ndatablocks = [encode(token) for token in tx_message]\ntx = b''.join(datablocks)\nprint('bytes on the wire:', hexlify(tx))\n\nassert hexlify(tx) == \\\n    b'220007c0010201220008d00102020030000c9801020a0020000fff220008d001020220'\n\n# decode bytes to message list\ndef decode(rx_bytes: bytes) -> List[Token]:\n    message: List[Token] = []\n\n    raw_datablocks = RawDatablock.parse(Bits.from_bytes(tx))\n    assert not isinstance(raw_datablocks, ValueError)\n    for db in raw_datablocks:\n        cat = db.get_category()\n        if cat == 34:\n            result034 = Cat034.cv_uap.parse(db.get_raw_records())\n            assert not isinstance(result034, ValueError)\n            for rec034 in result034:\n                i000 = rec034.get_item('000')\n                assert i000 is not None\n                val = i000.as_uint()\n                if val == 1:\n                    message.append(NorthMarker())\n                elif val == 2:\n                    i020 = rec034.get_item('020')\n                    assert i020 is not None\n                    azimuth = i020.variation.content.as_quantity(\"\u00b0\")\n                    message.append(SectorCrossing(azimuth = azimuth))\n                else:\n                    pass\n        elif cat == 48:\n            result048 = Cat048.cv_uap.parse(db.get_raw_records())\n            assert not isinstance(result048, ValueError)\n            for rec048 in result048:\n                i040 = rec048.get_item('040')\n                i070 = rec048.get_item('070')\n                assert i040 is not None\n                assert i070 is not None\n                rho = i040.variation.get_item('RHO').variation.content.as_quantity(\"NM\")\n                theta = i040.variation.get_item('THETA').variation.content.as_quantity(\"\u00b0\")\n                ssr = i070.variation.get_item('MODE3A').variation.content.as_string()\n                message.append(Plot(rho = rho, theta = theta, ssr = ssr))\n        else:\n            pass\n    return message\n\nrx = tx\nrx_message = decode(rx)\n\n# expect the same message\nprint('received message:', rx_message)\nassert rx_message == tx_message\n```\n\n## Installation\n\nUse any of the following methods:\n\n### Method 1\n\nInstall from python package index <https://pypi.org/project/libasterix/>:\n\n``` bash\npip install libasterix\n```\n\n### Method 2\n\nInstall from github:\n\n``` bash\n# (default branch)\npip install -e \"git+https://github.com/zoranbosnjak/asterix-libs.git#egg=libasterix&subdirectory=libs/python\"\n\n# ('devel' branch)\npip install -e \"git+https://github.com/zoranbosnjak/asterix-libs.git@devel#egg=libasterix&subdirectory=libs/python\"\n```\n\n### Method 3\n\nManually copy library files from [repository](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix).\n\nDownload and copy files either alongside your project sources or\nto some location where `python` can find it.\n\n```bash\n# check default python path\npython3 -c \"import sys; print('\\n'.join(sys.path))\"\n```\n\n## Tutorial\n\nCheck library installation.\n\n```bash\npython3 -c \"import asterix.base as base; print(base.AstSpec)\"\npython3 -c \"import asterix.generated as gen; print(gen.manifest['CATS'].keys())\"\n```\n\n### Import\n\nThis tutorial assumes importing complete `asterix` module into the current\nnamespace. In practice however only the required objects could be imported\nor the module might be imported to a dedicated namespace.\n\n```python\nfrom asterix.base import *\nfrom asterix.generated import *\n```\n\n### Error handling\n\nSome operation (eg. parsing) can fail on unexpected input. In such case,\nto indicate an error, this library will not raise an exception, but will\nreturn `ValueError('problem description')` instead.\n\nWith this approach, a user can handle errors in a type safe way, for example:\n\n```python\ndef parse_datablocks(s: bytes) -> List[RawDatablock]:\n    dbs = RawDatablock.parse(Bits.from_bytes(s))\n    if isinstance(dbs, ValueError):\n        return [] # or raise exception, or ...\n    return dbs\n```\n\nFor clarity, the error handling part is skipped in some parts of this tutorial.\n\n### Immutable objects\n\nAll operation on asterix objects are *immutable*.\n\nFor example:\n\n```python\n#| file: example-immutable.py\nfrom asterix.generated import *\n\nSpec = Cat_002_1_1\n\n# create empty record\nrec0 = Spec.cv_record.create({})\n\n# this operation does nothing (result is not stored)\nrec0.set_item('000', 1)\nassert rec0.get_item('000') is None\n\n# store result to 'rec1'\nrec1 = rec0.set_item('000', 1)\nassert rec1.get_item('000') is not None\n\n# del_item, store result to 'rec1a'\nrec1a = rec1.del_item('000')\nassert rec1a.get_item('000') is None\n\n# use multiple updates in sequence\nrec2a = rec0.set_item('000', 1).set_item('010', (('SAC', 1), ('SIC', 2)))\nrec2b = Spec.cv_record.create({'000': 1, '010': (('SAC', 1), ('SIC', 2))})\nassert rec2a.unparse() == rec2b.unparse()\n\n# mutation can be simulated by replacing old object with the new one\n# (using the same variable name)\nrec0 = rec0.set_item('000', 1)\nassert rec0.get_item('000') is not None\n```\n\n### Miscellaneous project and source code remarks\n\n- `cv_{name}` stands for *class variable*, to avoid name clash with\n  *instance variable* with the same name (which are without prefix).\n- `RuleContent` and `RuleVariation` are necessary to cope with some\n  small number of irregular cases with asterix definitions\n  (that is: context dependent definitions).\n- `NonSpare` is (as name suggests) an item with some defined content.\n  It is a separate class from `Item` and `Spare`, to reuse definition\n  in different contexts, for example `Compound` subitems are `NonSpare`.\n\n### Asterix specifications as python classes\n\nAsterix specifications hierarchy is reflected in python classes.\nFor example: Category `062` contains item `010`, which in turn contains\nsubitems `SAC` and `SIC`.\n\nThere is a `spec` class method which follows this structure deeper to the\nhierarchy. For example:\n\n```python\n#| file: example-spec.py\nfrom binascii import unhexlify\nfrom asterix.generated import *\n\n# Use cat062, edition 1.20\nSpec = Cat_062_1_20\n\nprint(Spec)\nprint('category number', Spec.cv_category)\nprint('edition', Spec.cv_edition)\n\n# Extract deeper specs\nUap = Spec.cv_uap\nRecord = Uap.cv_record\nI010 = Record.spec('010')\nSAC = I010.cv_rule.cv_variation.spec('SAC')\nprint(SAC)\n\n# Use more direct way to extract subspec\nSIC = Spec.cv_uap.cv_record.spec('010').cv_rule.cv_variation.spec('SIC')\nprint(SIC)\n\n# SAC and SIC subitems are both 8-bits long raw values (same structure),\n# so thay both map to the same class.\nassert (SAC==SIC)\n\n# With this specification it is possible to perform low level\n# asterix operations, for example to parse a single subitem\nsample_bits = Bits.from_bytes(unhexlify(b'ff0102fe'))\nprint(sample_bits)\nresult = SIC.parse(sample_bits)\nassert not isinstance(result, ValueError)\nsic_object, remaining_bits = result\nprint(remaining_bits)\nprint(sic_object)\nprint(sic_object.unparse())\nprint(sic_object.as_uint())\n\n# Similarly, it is possible to extract other parts, for example\n# extended subitems\nprint(Record.spec('080').cv_rule.cv_variation.spec('MON').cv_rule)\n# compound subitems\nprint(Record.spec('110').cv_rule.cv_variation.spec('SUM').cv_rule)\n```\n\nSee [base.py](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix/base.py)\nand [generated.py](https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python/src/asterix/generated.py)\nfor details.\n\n### Datagram\n\nDatagram is a raw binary data as received for example from UDP socket.\nThis is represented with `bytes` data type in python.\n\n### Raw Datablock\n\nRaw datablock is asterix datablock in the form `cat|length|data` with the\ncorrect byte size. A datagram can contain multiple datablocks.\n\nThis is represented in python with `class RawDatablock`.\n\nIn some cases it might be sufficient to work with raw datablocks, for example\nin the case of asterix category filtering. In this case, it is not required\nto fully parse asterix records.\n\n**Example**: Category filter, drop datablocks if category == 1\n\n```python\n#| file: example1.py\nfrom binascii import hexlify, unhexlify\nfrom asterix.base import *\n\ndef receive_from_udp() -> bytes: # UDP rx text function\n    return unhexlify(''.join([\n        '01000401', # cat1 datablock\n        '02000402', # cat2 datablock\n        ]))\n\ndef send_to_udp(s: bytes) -> None: # UDP tx test function\n    print(hexlify(s))\n\ninput_data = Bits.from_bytes(receive_from_udp())\nraw_datablocks = RawDatablock.parse(input_data) # can fail on wrong input\nassert not isinstance(raw_datablocks, ValueError)\nvalid_datablocks = [db.unparse().to_bytes() \\\n                    for db in raw_datablocks if db.get_category() != 1]\noutput_data = b''.join(valid_datablocks)\nsend_to_udp(output_data)\n```\n\n### Datablock, Record\n\nDatablock (represented as `class Datablock`) is a higher level, where we\nhave a guarantee that all containing records are semantically correct\n(asterix is fully parsed or correctly constructed).\n\nDatablock/Record is required to work with asterix items and subitems.\n\n**Example**: Create 2 records and combine them to a single datablock\n\n```python\n#| file: example2.py\nfrom binascii import hexlify\nfrom asterix.generated import *\n\nSpec = Cat_002_1_1 # use cat002, edition 1.1\n\nrec1 = Spec.cv_record.create({\n    '000': 1,\n    '010': (('SAC', 1), ('SIC', 2)),\n    })\n\nrec2 = Spec.cv_record.create({\n    '000': 2,\n    '010': (('SAC', 1), ('SIC', 2)),\n    })\n\ndb = Spec.create([rec1, rec2])\ns = db.unparse().to_bytes() # ready to send over the network\nprint(hexlify(s))\n```\n\n**Example**: Parse datagram (from the example above) and extract message type\nfrom each record\n\n```python\n#| file: example3.py\nfrom binascii import unhexlify\nfrom asterix.base import *\nfrom asterix.generated import *\n\nSpec = Cat_002_1_1 # use cat002, edition 1.1\n\ns = unhexlify(b'02000bc0010201c0010202') # ... use data from the example above\nraw_datablocks = RawDatablock.parse(Bits.from_bytes(s)) # can fail on wrong input\nassert not isinstance(raw_datablocks, ValueError)\nfor db in raw_datablocks:\n    records = Spec.cv_uap.parse(db.get_raw_records()) # can fail on wrong input\n    assert not isinstance(records, ValueError)\n    for record in records:\n        i000 = record.get_item('000') # returns None if the item is not present\n        assert i000 is not None\n        raw_value = i000.as_uint()\n        description = i000.variation.content.table_value()\n        print('{}: {}'.format(raw_value, description))\n```\n\n**Example**: Asterix filter, rewrite SAC/SIC code with random values.\n\n```python\n#| file: example4.py\nimport time\nimport random\nfrom asterix.base import *\nfrom asterix.generated import *\n\n# categories/editions of interest\nSpecs = {\n    48: Cat_048_1_31,\n    62: Cat_062_1_19,\n    63: Cat_063_1_6,\n    # ...\n    }\n\ndef process_record(sac, sic, rec):\n    \"\"\"Process single record.\"\"\"\n    return rec.set_item('010', (('SAC', sac), ('SIC', sic)))\n\ndef process_datablock(sac, sic, db):\n    \"\"\"Process single raw datablock.\"\"\"\n    cat = db.get_category()\n    Spec = Specs.get(cat)\n    if Spec is None:\n        return db\n    # second level of parsing (records are valid)\n    records = Spec.cv_uap.parse(db.get_raw_records())\n    new_records = [process_record(sac, sic, rec) for rec in records]\n    return Spec.create(new_records)\n\ndef rewrite_sac_sic(sac : int, sic : int, s : bytes) -> bytes:\n    \"\"\"Process datagram.\"\"\"\n    # first level of parsing (datablocks are valid)\n    raw_datablocks = RawDatablock.parse(Bits.from_bytes(s))\n    result = [process_datablock(sac, sic, db) for db in raw_datablocks]\n    output = b''.join([db.unparse().to_bytes() for db in result])\n    return output\n\ndef rx_bytes_from_the_network():\n    \"\"\"Dummy rx function (generate valid asterix datagram).\"\"\"\n    time.sleep(1)\n    Spec = Cat_048_1_31\n    rec = Spec.cv_record.create({'010': 0, '040': 0})\n    db1 = Spec.create([rec, rec]).unparse().to_bytes()\n    db2 = Spec.create([rec, rec]).unparse().to_bytes()\n    return b''.join([db1, db2])\n\ndef tx_bytes_to_the_network(s_output):\n    \"\"\"Dummy tx function.\"\"\"\n    print(hexlify(s_output))\n\n# main processing loop\ncnt = 0\nwhile True:\n    s_input = rx_bytes_from_the_network()\n    new_sac = random.randint(0,127)\n    new_sic = random.randint(128,255)\n    try:\n        s_output = rewrite_sac_sic(new_sac, new_sic, s_input)\n        tx_bytes_to_the_network(s_output)\n    except Exception as e:\n        print('Asterix exception: ', e)\n    # only run a few iterations for test\n    cnt += 1\n    if cnt > 3:\n        break\n```\n\n#### Spare items\n\nSome bits are defined as *Spare*, which are normally set to `0`.\nWith this library:\n\n- A user is able set spare bits to any value, including abusing spare bits\n  to contain non-zero value.\n- When parsing data, tolerate spare bits to contain any value. It is up\n  to the application to check the spare bits if desired.\n\nMultiple spare bit groups can be defined on a single item.\n`get_spares` method returns the actual values of all spare bit groups.\n\n**Example**\n\n```python\n#| file: example-spare.py\nfrom asterix.generated import *\n\n# I062/120 contain single group of spare bits\nSpec = Cat_062_1_20\n\n# create regular record with spare bits set to '0'\nrec1 = Spec.cv_record.create({\n    '120': (0, ('MODE2', 0x1234)),\n})\ni120a = rec1.get_item('120')\nassert i120a is not None\nspares1 = i120a.variation.get_spares()\nassert spares1 == [0]\n\n# create record, abuse spare bits, set to '0xf'\nrec2 = Spec.cv_record.create({\n    '120': (0xf, ('MODE2', 0x1234)),\n})\ni120b = rec2.get_item('120')\nassert i120b is not None\nspares2 = i120b.variation.get_spares()\nassert spares2 == [0xf]\n```\n\n#### Reserved expansion fields\n\nThis library supports working with expansion fields. From the `Record`\nprespective, the `RE` item contains raw bytes, without any structure,\nsimilar to how a datablock contains raw bytes without a structure. Parsing\nraw datablocks and parsing records are 2 separate steps. In the same\nvain, parsing `RE` out of the record would be a third step. Once parsed,\nthe `RE` item  gets it's structure, and it's possible to access it's subitems,\nsimilar to a regular record/subitem situation.\n\nWhen constructing a record with the `RE` item, a user must first\nconstruct the `RE` item itself, unparse it to bytes and insert bytes\nas a value of the `RE` item of a record.\n\nA reason for this separate stage approach is that a category and expansion\nspecification can remain separate to one another. In addition, a user has\na possiblity to explicitly select both editions individually.\n\nThis example demonstrates required steps for constructing and parsing:\n\n```python\n#| file: example5.py\nfrom asterix.generated import *\n\nSpec = Cat_062_1_20\nRef  = Ref_062_1_3\n\n# create 'RE' subitem\nref = Ref.cv_expansion.create({\n    'CST': [0],\n    'CSN': [1,2],\n    'V3': {\n        'PS3': 0,\n        },\n    })\n\n# create record, insert 'RE' subitem as bytes\nrec = Spec.cv_record.create({\n    '010': (('SAC', 1), ('SIC', 2)),\n    'RE': ref.unparse().to_bytes(),\n    })\n\ndb = Spec.create([rec])\ns = db.unparse()\nassert s.to_bytes().hex() == \\\n    '3e001b8101010104010211c8010000000000020000010000028000'\n\n# first stage, parse to the record\nraw_datablocks = RawDatablock.parse(s)\nassert not isinstance(raw_datablocks, ValueError)\nassert len(raw_datablocks) == 1 # expecting 1 datablock\nresult1 = Spec.cv_uap.parse(raw_datablocks[0].get_raw_records())\nassert not isinstance(result1, ValueError)\nassert len(result1) == 1 # expecting one record\n\n# get 'RE' subitem,\nre_subitem = result1[0].get_item('RE')\nassert re_subitem is not None\nre_bytes = re_subitem.variation.get_bytes()\n\n# second stage: parse 'RE' structure\nresult2 = Ref.cv_expansion.parse(Bits.from_bytes(re_bytes))\nassert not isinstance(result2, ValueError)\nref_readback, remaining = result2\nassert remaining.null()\n# expecting the same 'ref' as the original\nassert ref.unparse() == ref_readback.unparse()\n# we have a structure back and we can extract the values\nresult3 = ref_readback.get_item('CSN')\nassert result3 is not None\nlst = result3.variation.get_list()\nassert len(lst) == 2\nassert lst[0].as_uint() == 1\nassert lst[1].as_uint() == 2\n```\n\n#### Dependent specifications\n\nIn some rare cases, asterix definitions depend on a value of some other\nitem(s). In such cases, the asterix processing is more involved. This\ndependency manifests itself in two ways:\n\n- **content dependency**, where a content (interpretation of bits) of some\n  item depends on the value of some other item(s). For example:\n  `I062/380/IAS/IAS`, the structure is always 15 bits long,\n  but the interpretation of bits could be either speed in `NM/s` or `Mach`,\n  with different scaling factors, depending on the values of a sibling item.\n- **variation dependency**, where not only the content, but also a complete\n  item stucture depends on some other item(s). For example, the structure\n  of item `I004/120/CC/CPC` depends on 2 other item values.\n\nThis library can handle all structure cases, however it does not automatically\ncorrelate to the values of items that a structure depends on. When creating\nrecords, it is a user responsibility to properly set \"other item values\" for\na record to be valid.\nSimilarly, after a record is parsed, a user shall cast a default structure to a\ntarget structure, depending on the other items values. Whenever there is a\ndependency, there is also a statically known *default* structure, which is\nused during automatic record parsing.\n\n##### Handling **content dependency**\n\nThis example demonstrates how to work with **content dependency**,\nsuch as `I062/380/IAS`.\n\n```python\n#| file: dep-content.py\nfrom asterix.base import *\nfrom asterix.generated import *\n\nSpec = Cat_062_1_20 # Cat 062, edition 1.20\n\n# create records by different methods\n\n# set raw value\nrec0 = Spec.cv_record.create({\n    '380': {\n        'IAS': (\n            ('IM', 0),  # set IM to 0\n            ('IAS', 1)  # set IAS to raw value 1 (no unit conversion)\n        ) },\n    })\n\n# set raw value using default case\nrec1 = Spec.cv_record.create({\n    '380': {\n        'IAS': (\n            ('IM', 0),  # set IM to 0\n            ('IAS',\n                (None,  # use default raw case\n                 1))    # set IAS to raw value 1 (same as above)\n        ) },\n    })\n\n# set IAS speed (NM/s)\nrec2 = Spec.cv_record.create({\n    '380': {\n        'IAS': (\n            ('IM', 0), # airspeed = IAS\n            ('IAS',\n                ((0,), # use case with index 0 (IAS)\n                 (1.2, 'NM/s'))), # set IAS to 1.2 NM/s\n        ) },\n    })\n\n# set Mach speed\nrec3 = Spec.cv_record.create({\n    '380': {\n        'IAS': (\n            ('IM', 1), # airspeed = Mach\n            ('IAS',\n                ((1,), # use case with index 1 (Mach)\n                 (0.8, 'Mach'))), # set speed to 0.8 Mach\n        ) },\n    })\n\ndb = Spec.create([rec0, rec1, rec2, rec3])\nexpected_output = b'3e0017011010000101101000010110104ccd0110108320'\nassert hexlify(db.unparse().to_bytes()) == expected_output\n\n# parse and interpret data from the example above\ninput_bytes = unhexlify(expected_output)\nraw_datablocks = RawDatablock.parse(Bits.from_bytes(input_bytes))\nassert not isinstance(raw_datablocks, ValueError)\nfor db in raw_datablocks:\n    assert db.get_category() == 62\n    result = Spec.cv_uap.parse(db.get_raw_records())\n    assert not isinstance(result, ValueError)\n    for (cnt, rec) in enumerate(result):\n        i380 = rec.get_item('380')\n        assert i380 is not None\n        item_IAS1 = i380.variation.get_item('IAS')\n        assert item_IAS1 is not None\n        item_IM = item_IAS1.variation.get_item('IM')\n        item_IAS2 = item_IAS1.variation.get_item('IAS')\n        assert item_IM is not None\n        assert item_IAS2 is not None\n        match item_IM.as_uint(): # check value of I062/380/IAS/IM and convert\n            case 0: # this is IAS, convert to 'NM/s', use case with index (0,)\n                value = ('NM/s', item_IAS2.variation.rule.content((0,)).as_quantity('NM/s'))\n            case 1: # this is Mach, convert to 'Mach', use case with index (1,)\n                value = ('Mach', item_IAS2.variation.rule.content((1,)).as_quantity('Mach'))\n            case _:\n                raise Exception('unexpected value')\n        print('--- record', cnt, '---')\n        print('I062/380/IAS/IM raw value:', item_IM.as_uint())\n        print('I062/380/IAS/IAS raw value:', item_IAS2.as_uint())\n        print('converted value', value)\n```\n\n##### Handling **variation dependency**\n\nThis example demonstrates how to work with **variation dependency**,\nsuch as `I004/120/CC/CPC`.\n\n```python\n#| file: dep-variation.py\nfrom asterix.base import *\nfrom asterix.generated import *\n\nSpec = Cat_004_1_13 # Cat 004, edition 1.13\n\n# Item 'I004/120/CC/CPC' depends on I004/000 and I004/120/CC/TID values\n# Default case is: element3, raw, but there are many other cases.\n# See asterix specification for details.\n# This example handles the following cases:\n# case (5, 1): element 3, table\n# case (9, 2): group (('RAS', element1, table), spare 2)\n\n# case (0, 0) - invalid combination\nrec0 = Spec.cv_record.create({\n    '000': 0, # invalid value\n    '120': {\n        'CC': (\n            ('TID', 0),\n            ('CPC', 0), # set to raw value 0\n            ('CS', 0)\n            )\n        }\n    })\n\n# case (5, 1)\nrec1 = Spec.cv_record.create({\n    '000': 5, # Area Proximity Warning (APW)\n    '120': {\n        'CC': (\n            ('TID', 1),\n            ('CPC', 0), # structure is 'raw', set to 0\n            ('CS', 0)\n            )\n        }\n    })\n\n# case (9, 2)\n# get variation structure of case (9, 2)\nVar = Spec.cv_record.spec('120').cv_rule.cv_variation.spec('CC').\\\n    cv_rule.cv_variation.spec('CPC').spec((9,2))\n# and create object of that structure ('RAS' + spare item)\nobj = Var.create(( ('RAS', 1), 0))  # RAS = Stage Two Alert\n\n# insert object into the record (as uint)\nrec2 = Spec.cv_record.create({\n    '000': 9, # RIMCAS Arrival / Landing Monitor (ALM)\n    '120': {\n        'CC': (\n            ('TID', 2),\n            ('CPC', obj.as_uint()),\n            ('CS', 0)\n            )\n        }\n    })\n\ndb = Spec.create([rec0, rec1, rec2])\nexpected_output = b'040012412000400041200540104120094028'\nassert hexlify(db.unparse().to_bytes()) == expected_output\n\n# parse and interpret data from the example above\ninput_bytes = unhexlify(expected_output)\nraw_datablocks = RawDatablock.parse(Bits.from_bytes(input_bytes))\nassert not isinstance(raw_datablocks, ValueError)\nfor db in raw_datablocks:\n    assert db.get_category() == 4\n    result = Spec.cv_uap.parse(db.get_raw_records())\n    assert not isinstance(result, ValueError)\n    for (cnt, rec) in enumerate(result):\n        print('--- record', cnt, '---')\n        i000 = rec.get_item('000')\n        i120 = rec.get_item('120')\n        assert i000 is not None and i120 is not None\n        item_CC = i120.variation.get_item('CC')\n        assert item_CC is not None\n        item_TID = item_CC.variation.get_item('TID').as_uint()\n        item_CPC = item_CC.variation.get_item('CPC')\n        item_CS  = item_CC.variation.get_item('CS').as_uint()\n        index = (i000.as_uint(), item_TID)\n\n        try:\n            var_CPC = item_CPC.variation(index)\n        except Exception:\n            var_CPC = None\n        assert not isinstance(var_CPC, ValueError)\n\n        match index:\n            case (5, 1):\n                x = var_CPC.as_uint()\n                value = ('case 5,1', 'raw', x)\n            case (9, 2):\n                item_ras = var_CPC.get_item('RAS')\n                spares = var_CPC.get_spares()\n                value = ('case 9,2', 'RAS', item_ras.as_uint(), spares)\n            case _:\n                value = None\n        print(value)\n```\n\n#### Multiple UAP-s\n\nMake sure to use appropriate UAP name, together with a correct UAP selector\nvalue, for example for CAT001:\n\n- `['020', 'TYP'] = 0` for `plot`\n- `['020', 'TYP'] = 1` for `track`\n\n```python\n#| file: example6.py\nfrom asterix.base import *\nfrom asterix.generated import *\n\nCat1 = Cat_001_1_4\n\nrec01_plot = Cat1.cv_uap.spec('plot').create({\n    '010': 0x0102,\n    '020': ((('TYP',0),0,0,0,0,0,None),),\n    '040': 0x01020304\n})\n\nrec01_track = Cat1.cv_uap.spec('track').create({\n    '010': 0x0102,\n    '020': ((('TYP',1),0,0,0,0,0,None),),\n    '040': 0x01020304,\n    })\n\nrec01_invalid = Cat1.cv_uap.spec('plot').create({\n    '010': 0x0102,\n    '020': ((('TYP',1),0,0,0,0,0,None),),\n    '040': 0x01020304\n})\n\nprint(Cat1.create([rec01_plot]).unparse().to_bytes().hex())\nprint(Cat1.create([rec01_track]).unparse().to_bytes().hex())\nprint(Cat1.create([rec01_invalid]).unparse().to_bytes().hex())\n```\n\n### RFS handling\n\nThis library supports RFS mechanism for categories that include RFS\nindicator(s). For such cases, it is possible to sequence subitems in\nany order. Once such record is created or parsed, a user can extract\nsubitems using `get_rfs_item` method. The result in this case is\na list, since the item can be present in the record multiple times.\nAn empty list indicates that no such item is present in the RFS.\n\n**Example**\n\n```python\n#| file: example-rfs.py\nfrom binascii import hexlify\nfrom asterix.generated import *\n\n# cat008 contains RFS indicator, so we are able to add RFS items\nSpec = Cat_008_1_3\n\nrec1 = Spec.cv_record.create({\n    '000': 1,                                   # add item '000' (regular)\n    '010': (('SAC', 1), ('SIC', 2)),            # add item '010' (regular)\n    },\n    [\n        ('010', (('SAC', 3), ('SIC', 4))),      # add item '010' as RFS\n        ('010', (('SAC', 4), ('SIC', 5))),      # add another item '010' as RFS\n    ]\n    )\n\n# extract regular item 010\ni010_regular = rec1.get_item('010')\nprint(i010_regular)\n\n# extract RFS items 010, expecting 2 such items\ni010_rfs = rec1.get_rfs_item('010')\nassert len(i010_rfs) == 2\nfor i in i010_rfs:\n    print(i)\n\n# but item '000' is not present in RFS\nassert len(rec1.get_rfs_item('000')) == 0\n```\n\n### Strict and partial record parsing modes\n\nThis library supports parsing records strictly or partially.\n\nIn a strict mode, we want to make sure that all data is parsed exactly\nas specified in the particular category/edition schema. The record parsing\nfails if the FSPEC parsing fails or if any subsequent item parsing fails.\n\nI a partial mode, we don't require exact parsing match. If we know where\nin a bytestring a record starts, we can try to parse *some* information out of\nthe data stream, even in the case if the editions of the transmitter and the\nreceiver do not match exactly. In particular: if the transmitter sends some\nadditional items, unknown to the receiver. In that case, the receiver can still\nparse up to some point in a datablock.\n\nPartial record parsing means to parse the FSPEC (which might fail) followed\nby parsing subitems up to the point until items parsing is successful. The\nrecord parsing only fails if the FSPEC parsing itself fails.\n\nThis is useful in situations where a datablock contains only one record\n(known as *non-blocking* in Asterix Maintenance Group vocabulary) or if\nwe are interested only in the first record (even if there are more). The idea\nis to regain some forward compatibility on the receiver side, such that the\nreceiver does not need to upgrade edition immediately as the transmitter\nupgrades or even before that. Whether this is safe or not, depends on the\napplication and the exact differences between transmitter and receiver\nasterix editions.\n\nThe following parsing methods exist:\n\n- `UapClass.parse(s: Bits) -> ValueError or List[Record]`\n- `RecordClass.parse(pm: ParsingMode, s: Bits) -> ValueError or Record + remaining`\n\n`ParsingMode` is an `Enum` with the following options:\n\n- `StrictParsing`\n- `PartialParsing`\n\nCalling `parse` on some `Uap` class returns **list of records** on success.\nThis method always uses *strict* parsing and it makes sure it consumes all\ninput data.\n\nCalling `parse` on some `Record` class returns that record instance and\nthe remaining bytes. A method also requires parsing mode to be specified.\n\nBoth methods can fail on invalid input data (return `ValueError`).\n\nThis example demonstrates various parsing modes:\n\n```python\nSpec = Cat_NNN_E_E # some category/edition spec\n\ns: Bits = db.get_raw_records() # some input bits to be parsed\n\n# strictly parse records\n# 'parse' is called on 'Uap' class\n# successful result is a List of Records\nresult1 = Spec.cv_uap.parse(s)\nif not isinstance(result1, ValueError):\n    for r1 in result1:\n        print(r1)\n\n# strictly parse a single record\n# 'parse' is called on 'Record' class\n# successful result is (Record + remaining bytes)\nresult2 = Spec.cv_record.parse(ParsingMode.StrictParsing, s)\nif not isinstance(result2, ValueError):\n    r2, remaining = result2\n\n# partially parse a single record\n# successful result is (Record + remaining bytes),\nresult3 = Spec.cv_record.parse(ParsingMode.PartialParsing, s)\nif not isinstance(result3, ValueError):\n    r3, remaining = result3\n```\n\n### Library manifest\n\nThis library defines a `manifest` structure in the form:\n\n```python\nmanifest = {\n    'CATS': {\n        1: [\n            Cat_001_1_2,\n            Cat_001_1_3,\n            Cat_001_1_4,\n        ],\n        2: [\n            Cat_002_1_0,\n            Cat_002_1_1,\n        ],\n...\n```\n\nThis structure can be used to extract *latest* editions for each defined\ncategory, for example:\n\n```python\n#| file: example7.py\nfrom asterix.generated import *\n\nSpecs = {cat: manifest['CATS'][cat][-1] for cat in manifest['CATS']}\n\nfor spec in Specs.values():\n    print(spec.cv_category, spec.cv_edition)\n```\n\nAlternatively, a prefered way is to be explicit about each edition,\nfor example:\n\n```python\n#| file: example8.py\nfrom asterix.generated import *\n\nSpecs = {\n    48: Cat_048_1_31,\n    62: Cat_062_1_19,\n    63: Cat_063_1_6,\n    # ...\n    }\n```\n\n### Generic asterix processing\n\n*Generic processing* in this context means working with asterix data where\nthe subitem names and types are determined at runtime. That is: the explicit\nsubitem names are never mentioned in the application source code.\n\nThis is in contrast to *application specific processing*, where we are\nexplicit about subitems, for example [\"010\", \"SAC\"].\n\n**Example**: Show raw content of all toplevel items of each record\n\n```python\n#| file: example9.py\nfrom binascii import unhexlify\nfrom asterix.generated import *\n\nSpecs = {\n    48: Cat_048_1_31,\n    62: Cat_062_1_19,\n    63: Cat_063_1_6,\n    # ...\n}\n\n# some test input bytes\ns = unhexlify(''.join([\n    '3e00a5254327d835a95a0d0a2baf256af940e8a8d0caa1a594e1e525f2e32bc0448b',\n    '0e34c0b6211b5847038319d1b88d714b990a6e061589a414209d2e1d00ba5602248e',\n    '64092c2a0410138b2c030621c2043080fe06182ee40d2fa51078192cce70e9af5435',\n    'aeb2e3c74efc7107052ce9a0a721290cb5b2b566137911b5315fa412250031b95579',\n    '03ed2ef47142ed8a79165c82fb803c0e38c7f7d641c1a4a77740960737']))\n\ndef handle_nonspare(cat, name, nsp):\n    print('cat{}, item {}, {}'.format(cat, name, nsp.unparse()))\n    # depending on the application, we might want to display\n    # deep subitems, which is possible by examining 'nsp' object\n\nfor db in RawDatablock.parse(Bits.from_bytes(s)):\n    cat = db.get_category()\n    Spec = Specs.get(cat)\n    if Spec is None:\n        print('unsupported category', cat)\n        continue\n    for record in Spec.cv_uap.parse(db.get_raw_records()):\n        for (name, nsp) in record.items_regular.items():\n            handle_nonspare(cat, name, nsp)\n```\n\n**Example**: Generate dummy single record datablock with all fixed items set to zero\n\n```python\n#| file: example10.py\nfrom binascii import hexlify\nfrom asterix.generated import *\n\n# we could even randomly select a category/edition from the 'manifest',\n# but for simplicity just use a particular spec\nSpec = Cat_062_1_20\n\nrec = Spec.cv_record.create({})\nall_items = Spec.cv_record.cv_items_dict\nfor name in all_items:\n    if name is None:\n        continue\n    nsp = all_items[name]\n    var = nsp.cv_rule.cv_variation\n    if issubclass(var, Element):\n        rec = rec.set_item(name, 0)\n    elif issubclass(var, Group):\n        rec = rec.set_item(name, 0)\n    elif issubclass(var, Extended):\n        pass # skip for this test\n    elif issubclass(var, Repetitive):\n        pass # skip for this test\n    elif issubclass(var, Explicit):\n        pass # skip for this test\n    elif issubclass(var, Compound):\n        pass # skip for this test\n    else:\n        raise Exception('unexpected subclass')\n\ns = Spec.create([rec]).unparse().to_bytes()\nprint(hexlify(s))\n```\n\n## Using `mypy` static code checker\n\n**Note**: Tested with `mypy` version `1.9.0`.\n\n[mypy](https://www.mypy-lang.org/) is a static type checker for Python.\nIt is recommended to use the tool on asterix application code, to identify\nsome problems which would otherwise result in runtime errors.\n\nConsider the following test program (`test.py`):\n\n```python\nfrom asterix.generated import *\n\nSpec = Cat_008_1_3\nrec = Spec.cv_record.create({'010': (('SA',1), ('SIC',2))})\ni010 = rec.get_item('010')\nprint(i010.variation.get_item('SA').as_uint())\n```\n\nThe program contains the following bugs:\n- Misspelled item name, `SA` instead of `SAC`, on lines 4 and 5\n- `get_item('010')` result is not checked if the item\n  is actually present, which might result in runtime error\n\n```\n$ python test.py\n... results in runtime error (wrong item name)\n$ pip install mypy\n$ mypy test.py\n... detects all problems, without actually running the program\nFound 3 errors in 1 file (checked 1 source file)\n```\n\nCorrect version of this program is:\n\n```python\n#| file: example11.py\nfrom asterix.generated import *\n\nSpec = Cat_008_1_3\nrec = Spec.cv_record.create({'010': (('SAC',1), ('SIC',2))})\ni010 = rec.get_item('010')\nif i010 is not None:\n    print(i010.variation.get_item('SAC').as_uint())\n```\n\n```\n$ mypy test.py\nSuccess: no issues found in 1 source file\n$ python test.py\n1\n```\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Asterix data processing library",
    "version": "0.21.0",
    "project_urls": {
        "Bug Tracker": "https://github.com/zoranbosnjak/asterix-libs/issues",
        "Homepage": "https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/python#readme"
    },
    "split_keywords": [
        "asterix",
        " eurocontrol",
        " radar"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "e5ae379c56f1ba88a68e6a1313336d34ff2cd4c4da6019862fe20a1162ade7ce",
                "md5": "d2a6edddcaa0adefcb9f172256ede8c3",
                "sha256": "c5c560394d80ddf1e1ff8a41733eafdbe0a5eda1407772ce45cb0f8cdf42b2cc"
            },
            "downloads": -1,
            "filename": "libasterix-0.21.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d2a6edddcaa0adefcb9f172256ede8c3",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 524510,
            "upload_time": "2025-08-21T08:42:00",
            "upload_time_iso_8601": "2025-08-21T08:42:00.215137Z",
            "url": "https://files.pythonhosted.org/packages/e5/ae/379c56f1ba88a68e6a1313336d34ff2cd4c4da6019862fe20a1162ade7ce/libasterix-0.21.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "f7a26fc55c2a1fce50261f740cf07fe37ff19d95554723694692e42e09d88022",
                "md5": "04417999d921c70e579189de0e60fca7",
                "sha256": "a99543ce071feda2c7725c9a9a8dc3bb0951b6816efb8b9727a62cb1bc37b819"
            },
            "downloads": -1,
            "filename": "libasterix-0.21.0.tar.gz",
            "has_sig": false,
            "md5_digest": "04417999d921c70e579189de0e60fca7",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 529810,
            "upload_time": "2025-08-21T08:42:01",
            "upload_time_iso_8601": "2025-08-21T08:42:01.538828Z",
            "url": "https://files.pythonhosted.org/packages/f7/a2/6fc55c2a1fce50261f740cf07fe37ff19d95554723694692e42e09d88022/libasterix-0.21.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-21 08:42:01",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "zoranbosnjak",
    "github_project": "asterix-libs",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "libasterix"
}
        
Elapsed time: 3.94257s