libasterix 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2024, Zoran Bošnjak
2
+
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above
11
+ copyright notice, this list of conditions and the following
12
+ disclaimer in the documentation and/or other materials provided
13
+ with the distribution.
14
+
15
+ * Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived
17
+ from this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,651 @@
1
+ Metadata-Version: 2.1
2
+ Name: libasterix
3
+ Version: 0.1.0
4
+ Summary: Asterix data processing library
5
+ Author-email: Zoran Bošnjak <zoran.bosnjak@via.si>
6
+ Project-URL: Homepage, https://zoranbosnjak.github.io/asterix-libs
7
+ Project-URL: Bug Tracker, https://github.com/zoranbosnjak/asterix-libs/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: typing_extensions; python_version < "3.10"
15
+
16
+ # Asterix data processing library for python
17
+
18
+ Features:
19
+
20
+ - asterix data parsing/decoding from bytes
21
+ - asterix data encoding/unparsing to bytes
22
+ - precise conversion functions for physical quantities
23
+ - support for many asterix categories and editions
24
+ - support for Reserved Expansion Fields (REF)
25
+ - support for categories with multiple UAPs, eg. cat001
26
+ - support for context dependent items, eg. I062/380/IAS
27
+ - pure python implementation
28
+ - type annotations for static type checking,
29
+ including subitem access by name
30
+
31
+ ## Example
32
+
33
+ Encoding and decoding asterix example.
34
+ This example also includes type annotations for static
35
+ type checking with `mypy`. In a simple untyped environment,
36
+ the type annotations and assertions could be skipped.
37
+
38
+ ```python
39
+ from typing import *
40
+ from binascii import hexlify, unhexlify
41
+ from dataclasses import dataclass
42
+
43
+ from asterix.base import *
44
+ import asterix.generated as gen
45
+
46
+ # Select particular asterix categories and editions
47
+ Cat034 = gen.Cat_034_1_29
48
+ Cat048 = gen.Cat_048_1_32
49
+
50
+ # Example messages for this application
51
+ class Token:
52
+ pass
53
+
54
+ @dataclass
55
+ class NorthMarker(Token):
56
+ pass
57
+
58
+ @dataclass
59
+ class SectorCrossing(Token):
60
+ azimuth: float
61
+
62
+ @dataclass
63
+ class Plot(Token):
64
+ rho: float
65
+ theta: float
66
+ ssr: str
67
+
68
+ # example message to be encoded
69
+ tx_message = [
70
+ NorthMarker(),
71
+ SectorCrossing(0.0),
72
+ Plot(rho=10.0, theta=45.0, ssr='7777'),
73
+ SectorCrossing(45.0),
74
+ ]
75
+ print('sending message:', tx_message)
76
+
77
+ # encode token to datablock
78
+ def encode(token: Token) -> bytes:
79
+ if isinstance(token, NorthMarker):
80
+ rec034 = Cat034.cv_record.create({
81
+ '000': 1, # North marker message
82
+ '010': (('SAC', 1), ('SIC', 2)),
83
+ })
84
+ datablock034 = Cat034.create([rec034])
85
+ return datablock034.unparse().to_bytes()
86
+ if isinstance(token, SectorCrossing):
87
+ rec034 = Cat034.cv_record.create({
88
+ '000': 2, # Sector crossing message
89
+ '010': (('SAC', 1), ('SIC', 2)),
90
+ '020': ((token.azimuth, "°")),
91
+ })
92
+ datablock034 = Cat034.create([rec034])
93
+ return datablock034.unparse().to_bytes()
94
+ if isinstance(token, Plot):
95
+ rec048 = Cat048.cv_record.create({
96
+ '010': (('SAC', 1), ('SIC', 2)),
97
+ '040': (('RHO', (token.rho, "NM")), ('THETA', (token.theta, "°"))),
98
+ '070': (0, 0, 0, 0, ('MODE3A', token.ssr)),
99
+ })
100
+ datablock048= Cat048.create([rec048])
101
+ return datablock048.unparse().to_bytes()
102
+ raise Exception('unexpected token', token)
103
+
104
+ datablocks = [encode(token) for token in tx_message]
105
+ tx = b''.join(datablocks)
106
+ print('bytes on the wire:', hexlify(tx))
107
+
108
+ assert hexlify(tx) == \
109
+ b'220007c0010201220008d00102020030000c9801020a0020000fff220008d001020220'
110
+
111
+ # decode bytes to message list
112
+ def decode(rx_bytes: bytes) -> List[Token]:
113
+ message: List[Token] = []
114
+
115
+ raw_datablocks = RawDatablock.parse(Bits.from_bytes(tx))
116
+ assert not isinstance(raw_datablocks, ValueError)
117
+ for db in raw_datablocks:
118
+ cat = db.get_category()
119
+ if cat == 34:
120
+ result034 = Cat034.cv_uap.parse(db.get_raw_records())
121
+ assert not isinstance(result034, ValueError)
122
+ for rec034 in result034:
123
+ i000 = rec034.get_item('000')
124
+ assert i000 is not None
125
+ val = i000.as_uint()
126
+ if val == 1:
127
+ message.append(NorthMarker())
128
+ elif val == 2:
129
+ i020 = rec034.get_item('020')
130
+ assert i020 is not None
131
+ azimuth = i020.variation.content.as_quantity("°")
132
+ message.append(SectorCrossing(azimuth = azimuth))
133
+ else:
134
+ pass
135
+ elif cat == 48:
136
+ result048 = Cat048.cv_uap.parse(db.get_raw_records())
137
+ assert not isinstance(result048, ValueError)
138
+ for rec048 in result048:
139
+ i040 = rec048.get_item('040')
140
+ i070 = rec048.get_item('070')
141
+ assert i040 is not None
142
+ assert i070 is not None
143
+ rho = i040.variation.get_item('RHO').variation.content.as_quantity("NM")
144
+ theta = i040.variation.get_item('THETA').variation.content.as_quantity("°")
145
+ ssr = i070.variation.get_item('MODE3A').variation.content.as_string()
146
+ message.append(Plot(rho = rho, theta = theta, ssr = ssr))
147
+ else:
148
+ pass
149
+ return message
150
+
151
+ rx = tx
152
+ rx_message = decode(rx)
153
+
154
+ # expect the same message
155
+ print('received message:', rx_message)
156
+ assert rx_message == tx_message
157
+ ```
158
+
159
+ ## Installation
160
+
161
+ Use any of the following methods:
162
+
163
+ ### Method 1 - copy library files
164
+
165
+ The following files are required:
166
+
167
+ - [base.py](src/asterix/base.py)
168
+ - [generated.py](src/asterix/generated.py)
169
+
170
+ Download and copy files either alongside your project sources or
171
+ to some location where `python` can find it.
172
+
173
+ ```bash
174
+ # check default python path
175
+ python3 -c "import sys; print('\n'.join(sys.path))"
176
+ ```
177
+
178
+ ### Method 2 - install/update package with `pip`
179
+
180
+ Use `pip` to install or update:
181
+
182
+ ``` bash
183
+ # prepare and activate virtual environment (optional)
184
+ sudo apt install python3-venv
185
+ python3 -m venv env
186
+ source env/bin/activate
187
+
188
+ # install or upgrade (from default branch)
189
+ pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git#egg=libasterix&subdirectory=libs/python"
190
+
191
+ # install or upgrade (from 'devel' branch)
192
+ pip install -e "git+https://github.com/zoranbosnjak/asterix-libs.git@devel#egg=libasterix&subdirectory=libs/python"
193
+
194
+ # deactivate virtual environment when done (if activated)
195
+ deactivate
196
+ ```
197
+
198
+ ## Tutorial
199
+
200
+ Check library installation.
201
+
202
+ ```bash
203
+ python3 -c "import asterix.base as base; print(base.AstSpec)"
204
+ python3 -c "import asterix.generated as gen; print(gen.manifest['CATS'].keys())"
205
+ ```
206
+
207
+ ### Import
208
+
209
+ This tutorial assumes importing complete `asterix` module into the current
210
+ namespace. In practice however only the required objects could be imported
211
+ or the module might be imported to a dedicated namespace.
212
+
213
+ ```python
214
+ from asterix.base import *
215
+ from asterix.generated import *
216
+ ```
217
+
218
+ ### Error handling
219
+
220
+ Some operation (eg. parsing) can fail on unexpected input. In such case,
221
+ to indicate an error, this library will not raise an exception, but will
222
+ return `ValueError('problem description')` instead.
223
+
224
+ With this approach, a user can handle errors in a type safe way, for example:
225
+
226
+ ```python
227
+ def parse_datablocks(s: bytes) -> List[RawDatablock]:
228
+ dbs = RawDatablock.parse(Bits.from_bytes(s))
229
+ if isinstance(dbs, ValueError):
230
+ return [] # or raise exception, or ...
231
+ return dbs
232
+ ```
233
+
234
+ For clarity, the error handling part is skipped in some parts of this tutorial.
235
+
236
+ ### Immutable objects
237
+
238
+ All operation on asterix objects are *immutable*.
239
+
240
+ For example:
241
+
242
+ ```python
243
+ from asterix.generated import *
244
+
245
+ Spec = Cat_002_1_1
246
+
247
+ # create empty record
248
+ rec0 = Spec.cv_record.create({})
249
+
250
+ # this operation does nothing (result is not stored)
251
+ rec0.set_item('000', 1)
252
+ assert rec0.get_item('000') is None
253
+
254
+ # store result to 'rec1'
255
+ rec1 = rec0.set_item('000', 1)
256
+ assert rec1.get_item('000') is not None
257
+
258
+ # use multiple updates in sequence
259
+ rec2a = rec0.set_item('000', 1).set_item('010', (('SAC', 1), ('SIC', 2)))
260
+ rec2b = Spec.cv_record.create({'000': 1, '010': (('SAC', 1), ('SIC', 2))})
261
+ assert rec2a.unparse() == rec2b.unparse()
262
+
263
+ # mutation can be simulated by replacing old object with the new one
264
+ # (using the same variable name)
265
+ rec0 = rec0.set_item('000', 1)
266
+ assert rec0.get_item('000') is not None
267
+ ```
268
+
269
+ ### Datagram
270
+
271
+ Datagram is a raw binary data as received for example from UDP socket.
272
+ This is represented with `bytes` data type in python.
273
+
274
+ ### Raw Datablock
275
+
276
+ Raw datablock is asterix datablock in the form `cat|length|data` with the
277
+ correct byte size. A datagram can contain multiple datablocks.
278
+
279
+ This is represented in python with `class RawDatablock`.
280
+
281
+ In some cases it might be sufficient to work with raw datablocks, for example
282
+ in the case of asterix category filtering. In this case, it is not required
283
+ to fully parse asterix records.
284
+
285
+ **Example**: Category filter, drop datablocks if category == 1
286
+
287
+ ```python
288
+ from binascii import hexlify, unhexlify
289
+ from asterix.base import *
290
+
291
+ def receive_from_udp(): # UDP rx text function
292
+ return unhexlify(''.join([
293
+ '01000401', # cat1 datablock
294
+ '02000402', # cat2 datablock
295
+ ]))
296
+
297
+ def send_to_udp(s): # UDP tx test function
298
+ print(hexlify(s))
299
+
300
+ input_data = Bits.from_bytes(receive_from_udp())
301
+ raw_datablocks = RawDatablock.parse(input_data) # can fail on wrong input
302
+ valid_datablocks = [db.unparse().to_bytes() \
303
+ for db in raw_datablocks if db.get_category() != 1]
304
+ output_data = b''.join(valid_datablocks)
305
+ send_to_udp(output_data)
306
+ ```
307
+
308
+ ### Datablock, Record
309
+
310
+ Datablock (represented as `class Datablock`) is a higher level, where we
311
+ have a guarantee that all containing records are semantically correct
312
+ (asterix is fully parsed or correctly constructed).
313
+
314
+ Datablock/Record is required to work with asterix items and subitems.
315
+
316
+ **Example**: Create 2 records and combine them to a single datablock
317
+
318
+ ```python
319
+ from asterix.generated import *
320
+
321
+ Spec = Cat_002_1_1 # use cat002, edition 1.1
322
+
323
+ rec1 = Spec.cv_record.create({
324
+ '000': 1,
325
+ '010': (('SAC', 1), ('SIC', 2)),
326
+ })
327
+
328
+ rec2 = Spec.cv_record.create({
329
+ '000': 2,
330
+ '010': (('SAC', 1), ('SIC', 2)),
331
+ })
332
+
333
+ db = Spec.create([rec1, rec2])
334
+ s = db.unparse().to_bytes() # ready to send over the network
335
+ print(hexlify(s))
336
+ ```
337
+
338
+ **Example**: Parse datagram (from the example above) and extract message type
339
+ from each record
340
+
341
+ ```python
342
+ from asterix.base import *
343
+ from asterix.generated import *
344
+
345
+ Spec = Cat_002_1_1 # use cat002, edition 1.1
346
+
347
+ s = unhexlify(b'02000bc0010201c0010202') # ... use data from the example above
348
+ raw_datablocks = RawDatablock.parse(Bits.from_bytes(s)) # can fail on wrong input
349
+ for db in raw_datablocks:
350
+ records = Spec.cv_uap.parse(db.get_raw_records()) # can fail on wrong input
351
+ for record in records:
352
+ i000 = record.get_item('000') # returns None if the item is not present
353
+ raw_value = i000.as_uint()
354
+ description = i000.variation.content.table_value()
355
+ print('{}: {}'.format(raw_value, description))
356
+ ```
357
+
358
+ **Example**: Asterix filter, rewrite SAC/SIC code with random values.
359
+
360
+ ```python
361
+ import time
362
+ import random
363
+ from asterix.base import *
364
+ from asterix.generated import *
365
+
366
+ # categories/editions of interest
367
+ Specs = {
368
+ 48: Cat_048_1_31,
369
+ 62: Cat_062_1_19,
370
+ 63: Cat_063_1_6,
371
+ # ...
372
+ }
373
+
374
+ def process_record(sac, sic, rec):
375
+ """Process single record."""
376
+ return rec.set_item('010', (('SAC', sac), ('SIC', sic)))
377
+
378
+ def process_datablock(sac, sic, db):
379
+ """Process single raw datablock."""
380
+ cat = db.get_category()
381
+ Spec = Specs.get(cat)
382
+ if Spec is None:
383
+ return db
384
+ # second level of parsing (records are valid)
385
+ records = Spec.cv_uap.parse(db.get_raw_records())
386
+ new_records = [process_record(sac, sic, rec) for rec in records]
387
+ return Spec.create(new_records)
388
+
389
+ def rewrite_sac_sic(sac : int, sic : int, s : bytes) -> bytes:
390
+ """Process datagram."""
391
+ # first level of parsing (datablocks are valid)
392
+ raw_datablocks = RawDatablock.parse(Bits.from_bytes(s))
393
+ result = [process_datablock(sac, sic, db) for db in raw_datablocks]
394
+ output = b''.join([db.unparse().to_bytes() for db in result])
395
+ return output
396
+
397
+ def rx_bytes_from_the_network():
398
+ """Dummy rx function (generate valid asterix datagram)."""
399
+ time.sleep(1)
400
+ Spec = Cat_048_1_31
401
+ rec = Spec.cv_record.create({'010': 0, '040': 0})
402
+ db1 = Spec.create([rec, rec]).unparse().to_bytes()
403
+ db2 = Spec.create([rec, rec]).unparse().to_bytes()
404
+ return b''.join([db1, db2])
405
+
406
+ def tx_bytes_to_the_network(s_output):
407
+ """Dummy tx function."""
408
+ print(hexlify(s_output))
409
+
410
+ # main processing loop
411
+ while True:
412
+ s_input = rx_bytes_from_the_network()
413
+ new_sac = random.randint(0,127)
414
+ new_sic = random.randint(128,255)
415
+ try:
416
+ s_output = rewrite_sac_sic(new_sac, new_sic, s_input)
417
+ tx_bytes_to_the_network(s_output)
418
+ except Exception as e:
419
+ print('Asterix exception: ', e)
420
+ ```
421
+
422
+ #### Reserved expansion fields
423
+
424
+ TODO: Add parsing/constructing expansion field example
425
+
426
+ #### Multiple UAP-s
427
+
428
+ Make sure to use appropriate UAP name, together with a correct UAP selector
429
+ value, for example for CAT001:
430
+
431
+ - `['020', 'TYP'] = 0` for `plot`
432
+ - `['020', 'TYP'] = 1` for `track`
433
+
434
+ ```python
435
+ from asterix.base import *
436
+ from asterix.generated import *
437
+
438
+ Cat1 = Cat_001_1_4
439
+
440
+ rec01_plot = Cat1.cv_uap.spec('plot').create({
441
+ '010': 0x0102,
442
+ '020': ((('TYP',0),0,0,0,0,0,None),),
443
+ '040': 0x01020304
444
+ })
445
+
446
+ rec01_track = Cat1.cv_uap.spec('track').create({
447
+ '010': 0x0102,
448
+ '020': ((('TYP',1),0,0,0,0,0,None),),
449
+ '040': 0x01020304,
450
+ })
451
+
452
+ rec01_invalid = Cat1.cv_uap.spec('plot').create({
453
+ '010': 0x0102,
454
+ '020': ((('TYP',1),0,0,0,0,0,None),),
455
+ '040': 0x01020304
456
+ })
457
+
458
+ print(Cat1.create([rec01_plot]).unparse().to_bytes().hex())
459
+ print(Cat1.create([rec01_track]).unparse().to_bytes().hex())
460
+ print(Cat1.create([rec01_invalid]).unparse().to_bytes().hex())
461
+ ```
462
+
463
+ ### Library manifest
464
+
465
+ This library defines a `manifest` structure in the form:
466
+
467
+ ```python
468
+ manifest = {
469
+ 'CATS': {
470
+ 1: {
471
+ '1.2': CAT_001_1_2,
472
+ '1.3': CAT_001_1_3,
473
+ '1.4': CAT_001_1_4,
474
+ },
475
+ 2: {
476
+ '1.0': CAT_002_1_0,
477
+ '1.1': CAT_002_1_1,
478
+ #...
479
+ ```
480
+
481
+ This structure can be used to extract *latest* editions for each defined
482
+ category, for example:
483
+
484
+ ```python
485
+ from asterix.generated import *
486
+
487
+ def to_edition(ed):
488
+ """Convert edition string to a tuple, for example "1.2" -> (1,2)"""
489
+ a,b = ed.split('.')
490
+ return (int(a), int(b))
491
+
492
+ def get_latest_edition(lst):
493
+ return sorted(lst, key=lambda pair: to_edition(pair[0]), reverse=True)[0]
494
+
495
+ Specs = {} # will be populated with latest editions
496
+
497
+ for cat in range(1,256):
498
+ editions = manifest['CATS'].get(cat)
499
+ if editions is None:
500
+ continue
501
+ latest = get_latest_edition(editions.items())
502
+ ed, cls = latest
503
+ Specs[cat] = cls
504
+
505
+ print(Specs)
506
+ ```
507
+
508
+ Alternatively, a prefered way is to be explicit about each edition,
509
+ for example:
510
+
511
+ ```python
512
+ from asterix.generated import *
513
+
514
+ Specs = {
515
+ 48: Cat_048_1_31,
516
+ 62: Cat_062_1_19,
517
+ 63: Cat_063_1_6,
518
+ # ...
519
+ }
520
+ ```
521
+
522
+ ### Generic asterix processing
523
+
524
+ *Generic processing* in this context means working with asterix data where
525
+ the subitem names and types are determined at runtime. That is: the explicit
526
+ subitem names are never mentioned in the application source code.
527
+
528
+ This is in contrast to *application specific processing*, where we are
529
+ explicit about subitems, for example ["010", "SAC"].
530
+
531
+ **Example**: Show raw content of all toplevel items of each record
532
+
533
+ ```python
534
+ from asterix.generated import *
535
+
536
+ Specs = {
537
+ 48: Cat_048_1_31,
538
+ 62: Cat_062_1_19,
539
+ 63: Cat_063_1_6,
540
+ # ...
541
+ }
542
+
543
+ # some test input bytes
544
+ s = unhexlify(''.join([
545
+ '3e00a5254327d835a95a0d0a2baf256af940e8a8d0caa1a594e1e525f2e32bc0448b',
546
+ '0e34c0b6211b5847038319d1b88d714b990a6e061589a414209d2e1d00ba5602248e',
547
+ '64092c2a0410138b2c030621c2043080fe06182ee40d2fa51078192cce70e9af5435',
548
+ 'aeb2e3c74efc7107052ce9a0a721290cb5b2b566137911b5315fa412250031b95579',
549
+ '03ed2ef47142ed8a79165c82fb803c0e38c7f7d641c1a4a77740960737']))
550
+
551
+ def handle_nonspare(cat, name, nsp):
552
+ print('cat{}, item {}, {}'.format(cat, name, nsp.unparse()))
553
+ # depending on the application, we might want to display
554
+ # deep subitems, which is possible by examining 'nsp' object
555
+
556
+ for db in RawDatablock.parse(Bits.from_bytes(s)):
557
+ cat = db.get_category()
558
+ Spec = Specs.get(cat)
559
+ if Spec is None:
560
+ print('unsupported category', cat)
561
+ continue
562
+ for record in Spec.cv_uap.parse(db.get_raw_records()):
563
+ for (name, nsp) in record.items_regular.items():
564
+ handle_nonspare(cat, name, nsp)
565
+ ```
566
+
567
+ **Example**: Generate dummy single record datablock with all fixed items set to zero
568
+
569
+ ```python
570
+ from asterix.generated import *
571
+
572
+ # we could even randomly select a category/edition from the 'manifest',
573
+ # but for simplicity just use a particular spec
574
+ Spec = Cat_062_1_20
575
+
576
+ rec = Spec.cv_record.create({})
577
+ all_items = Spec.cv_record.cv_items_dict
578
+ for name in all_items:
579
+ if name is None:
580
+ continue
581
+ nsp = all_items[name]
582
+ var = nsp.cv_rule.cv_variation
583
+ if issubclass(var, Element):
584
+ rec = rec.set_item(name, 0)
585
+ elif issubclass(var, Group):
586
+ rec = rec.set_item(name, 0)
587
+ elif issubclass(var, Extended):
588
+ pass # skip for this test
589
+ elif issubclass(var, Repetitive):
590
+ pass # skip for this test
591
+ elif issubclass(var, Explicit):
592
+ pass # skip for this test
593
+ elif issubclass(var, Compound):
594
+ pass # skip for this test
595
+ else:
596
+ raise Exception('unexpected subclass')
597
+
598
+ s = Spec.create([rec]).unparse().to_bytes()
599
+ print(hexlify(s))
600
+ ```
601
+
602
+ ## Using `mypy` static code checker
603
+
604
+ **Note**: `mypy` version `0.991` or above is required for this library.
605
+
606
+ [mypy](https://www.mypy-lang.org/) is a static type checker for Python.
607
+ It is recommended to use the tool on asterix application code, to identify
608
+ some problems which would otherwise result in runtime errors.
609
+
610
+ Consider the following test program (`test.py`):
611
+
612
+ ```python
613
+ from asterix.generated import *
614
+
615
+ Spec = Cat_008_1_3
616
+ rec = Spec.cv_record.create({'010': (('SA',1), ('SIC',2))})
617
+ print(rec.get_item('010').variation.get_item('SA').as_uint())
618
+ ```
619
+
620
+ The program contains the following bugs:
621
+ - Misspelled item name, `SA` instead of `SAC`, on lines 4 and 5
622
+ - `get_item('010') result is not checked if the item
623
+ is actually present, which might result in runtime error
624
+
625
+ ```
626
+ $ python test.py
627
+ ... results in runtime error (wrong item name)
628
+ $ pip install mypy
629
+ $ mypy test.py
630
+ ... detects all problems, without actually running the program
631
+ Found 3 errors in 1 file (checked 1 source file)
632
+ ```
633
+
634
+ Correct version of this program is:
635
+
636
+ ```python
637
+ from asterix.generated import *
638
+
639
+ Spec = Cat_008_1_3
640
+ rec = Spec.cv_record.create({'010': (('SAC',1), ('SIC',2))})
641
+ i010 = rec.get_item('010')
642
+ if i010 is not None:
643
+ print(i010.variation.get_item('SAC').as_uint())
644
+ ```
645
+
646
+ ```
647
+ $ mypy test.py
648
+ Success: no issues found in 1 source file
649
+ $ python test.py
650
+ 1
651
+ ```