Skip to content
This repository was archived by the owner on Dec 26, 2025. It is now read-only.

Commit 8fa20a6

Browse files
committed
Added adi.loadi() to be able to efficiently watch ADI files for changes
1 parent a599527 commit 8fa20a6

File tree

3 files changed

+122
-19
lines changed

3 files changed

+122
-19
lines changed

src/adif_file/adi.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,34 @@ def unpack(data: str) -> dict:
7979
return unpacked
8080

8181

82-
def loads(adi: str) -> dict:
82+
def loadi(adi: str, skip: int = 0) -> Iterator[dict]:
83+
"""Turn ADI formated string to header/records as an iterator over dict
84+
The skip option is useful if you want to watch a file for new records only. This saves processing time.
85+
86+
:param adi: the ADI data
87+
:param skip: skip first number of records (does not apply for header)
88+
:return: an iterator of records (first record is the header even if not available)
89+
"""
90+
91+
record_data = adi
92+
if not adi.startswith('<'): # If a header is available
93+
hr_list = re.split(r'<[eE][oO][hH]>', adi)
94+
if len(hr_list) > 2:
95+
raise TooMuchHeadersException()
96+
97+
yield unpack(hr_list[0])
98+
record_data = hr_list[1]
99+
else: # Empty record for missing header
100+
yield {}
101+
102+
i = 0
103+
for rec in re.finditer(r'(.*?)<[eE][oO][rR]>', record_data, re.S):
104+
if i >= skip:
105+
yield unpack(rec.groups()[0])
106+
i += 1
107+
108+
109+
def loads(adi: str, skip: int = 0) -> dict:
83110
"""Turn ADI formated string to dictionary
84111
The parameters are converted to uppercase
85112
@@ -88,48 +115,50 @@ def loads(adi: str) -> dict:
88115
'RECORDS': [list of records]
89116
}
90117
118+
The skip option is useful if you want to watch a file for new records only. This saves processing time.
119+
In this case consider to use loadi() directly.
120+
91121
:param adi: the ADI data
122+
:param skip: skip first number of records (does not apply for header)
92123
:return: the ADI as a dict
93124
"""
94125

95126
doc = {'HEADER': None,
96127
'RECORDS': []
97128
}
98129

99-
record_data = adi
100-
if not adi.startswith('<'):
101-
hr_list = re.split(r'<[eE][oO][hH]>', adi)
102-
if len(hr_list) > 2:
103-
raise TooMuchHeadersException()
104-
105-
doc['HEADER'] = unpack(hr_list[0])
106-
record_data = hr_list[1]
107-
108-
rec_list = re.split(r'<[eE][oO][rR]>', record_data)
109-
for rec in rec_list:
110-
if rec.strip():
111-
doc['RECORDS'].append(unpack(rec))
130+
first = True
131+
for rec in loadi(adi, skip):
132+
if first:
133+
doc['HEADER'] = rec
134+
first = False
135+
else:
136+
doc['RECORDS'].append(rec)
112137

113138
return doc
114139

115140

116-
def load(file_name: str):
117-
"""Turn ADI formated string to dictionary
141+
def load(file_name: str, skip: int = 0) -> dict:
142+
"""Load ADI formated file to dictionary
118143
The parameters are converted to uppercase
119144
120145
{
121146
'HEADER': None,
122147
'RECORDS': [list of records]
123148
}
124149
150+
The skip option is useful if you want to watch a file for new records only. This saves processing time.
151+
In this case consider to use loadi() directly.
152+
125153
:param file_name: the file name where the ADI data is stored
154+
:param skip: skip first number of records (does not apply for header)
126155
:return: the ADI as a dict
127156
"""
128157

129158
with open(file_name, encoding='ascii') as af:
130159
data = af.read()
131160

132-
return loads(data)
161+
return loads(data, skip)
133162

134163

135164
def pack(param: str, value: str, dtype: str = None) -> str:
@@ -257,7 +286,7 @@ def dump(file_name: str, data_dict: dict, comment: str = 'ADIF export by ' + __p
257286
af.write(chunk)
258287

259288

260-
__all__ = ['load', 'loads', 'dump', 'dumps',
289+
__all__ = ['load', 'loads', 'loadi', 'dump', 'dumps', 'dumpi',
261290
'TooMuchHeadersException', 'TagDefinitionException',
262291
'IllegalDataTypeException', 'IllegalParameterException',
263292
'StringNotASCIIException']

test/test_loadadi.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import adif_file.adi
55

66

7-
def get_file_path(file):
7+
def get_file_path(file: str):
88
return os.path.join(os.path.dirname(__file__), file)
99

1010

@@ -54,10 +54,59 @@ def test_50_goodfile(self):
5454
self.assertEqual(3, len(adi_dict['HEADER']))
5555
self.assertEqual(5, len(adi_dict['RECORDS']))
5656

57+
def test_52_no_header(self):
58+
adi_dict = adif_file.adi.load(get_file_path('testdata/goodfile_no_h.txt'))
59+
60+
self.assertIn('HEADER', adi_dict)
61+
self.assertIn('RECORDS', adi_dict)
62+
self.assertEqual(0, len(adi_dict['HEADER']))
63+
self.assertEqual(5, len(adi_dict['RECORDS']))
64+
5765
def test_55_toomuchheaders(self):
5866
self.assertRaises(adif_file.adi.TooMuchHeadersException, adif_file.adi.load,
5967
get_file_path('testdata/toomuchheadersfile.txt'))
6068

69+
def test_60_skiprecords(self):
70+
adi_dict = adif_file.adi.load(get_file_path('testdata/goodfile.txt'), 3)
71+
rec_dict = {'QSO_DATE': '20231008', 'TIME_ON': '1754', 'RST_SENT': '59',
72+
'RST_RCVD': '59', 'BAND': '2190M', 'MODE': 'AM', 'FREQ': '0.137',
73+
'TX_PWR': '4.0', 'STATION_CALLSIGN': 'XX1XXX', 'MY_GRIDSQUARE': 'JO35uj27'}
74+
75+
self.assertIn('HEADER', adi_dict)
76+
self.assertIn('RECORDS', adi_dict)
77+
self.assertEqual(3, len(adi_dict['HEADER']))
78+
self.assertEqual(2, len(adi_dict['RECORDS']))
79+
self.assertDictEqual(rec_dict, adi_dict['RECORDS'][0])
80+
81+
def test_65_skiprec_noh(self):
82+
adi_dict = adif_file.adi.load(get_file_path('testdata/goodfile_no_h.txt'), 3)
83+
rec_dict = {'QSO_DATE': '20231008', 'TIME_ON': '1754', 'RST_SENT': '59',
84+
'RST_RCVD': '59', 'BAND': '2190M', 'MODE': 'AM', 'FREQ': '0.137',
85+
'TX_PWR': '4.0', 'STATION_CALLSIGN': 'XX1XXX', 'MY_GRIDSQUARE': 'JO35uj27'}
86+
87+
self.assertIn('HEADER', adi_dict)
88+
self.assertIn('RECORDS', adi_dict)
89+
self.assertEqual(0, len(adi_dict['HEADER']))
90+
self.assertEqual(2, len(adi_dict['RECORDS']))
91+
self.assertDictEqual(rec_dict, adi_dict['RECORDS'][0])
92+
93+
def test_70_loadi(self):
94+
adi_txt = '''<QSO_DATE:8>20231008 <TIME_ON:4>1145 <CALL:6>dl4bdf<eor>
95+
<QSO_DATE:8>20231008 <TIME_ON:4>1146 <CALL:6>DL5HJK <NAME:5>Peter <eor>
96+
<QSO_DATE:8>20231008 <TIME_ON:4>1340 <RST_SENT:2>59<eor>
97+
<QSO_DATE:8>20231008 <TIME_ON:4>1754<eor>
98+
<eor>
99+
<QSO_DATE:8>20231008 <MODE:2>AM <eor>'''
100+
101+
rec_list = ({},
102+
{'QSO_DATE': '20231008', 'TIME_ON': '1340', 'RST_SENT': '59'},
103+
{'QSO_DATE': '20231008', 'TIME_ON': '1754'},
104+
{},
105+
{'QSO_DATE': '20231008', 'MODE': 'AM'})
106+
107+
for exp, rec in zip(rec_list, adif_file.adi.loadi(adi_txt, 2)):
108+
self.assertDictEqual(exp, rec)
109+
61110

62111
if __name__ == '__main__':
63112
unittest.main()

test/testdata/goodfile_no_h.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<QSO_DATE:8>20231008 <TIME_ON:4>1145 <CALL:6>dl4bdf <NAME:6>Walter <QTH:8>Dortmund <GRIDSQUARE:8>Jo30Uj45
2+
<BAND:3>80M <MODE:2>AM <FREQ:5>4.000
3+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27 <DISTANCE:1>2
4+
<NOTES:5>Test1
5+
<eor>
6+
7+
<QSO_DATE:8>20231008 <TIME_ON:4>1146 <CALL:6>DL5HJK <NAME:5>Peter <QTH:13>Welschneudorf <GRIDSQUARE:8>Jo30uj12
8+
<RST_SENT:2>59 <RST_RCVD:2>47 <BAND:4>630M <MODE:2>AM <TX_PWR:3>4.0
9+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27 <DISTANCE:1>2
10+
<eor>
11+
12+
<QSO_DATE:8>20231008 <TIME_ON:4>1340
13+
<RST_SENT:2>59 <RST_RCVD:2>59 <BAND:4>630M <MODE:2>AM <FREQ:5>0.472 <TX_PWR:3>4.0
14+
<MY_GRIDSQUARE:8>JO35uj27
15+
<eor>
16+
17+
<QSO_DATE:8>20231008 <TIME_ON:4>1754
18+
<RST_SENT:2>59 <RST_RCVD:2>59 <BAND:5>2190M <MODE:2>AM <FREQ:5>0.137 <TX_PWR:3>4.0
19+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27
20+
<eor>
21+
22+
<QSO_DATE:8>20231008 <TIME_ON:4>1755
23+
<RST_SENT:2>59 <RST_RCVD:2>59 <BAND:3>12M <MODE:2>AM <FREQ:6>24.790 <TX_PWR:3>4.0
24+
<STATION_CALLSIGN:6>XX1XXX <MY_GRIDSQUARE:8>JO35uj27
25+
<eor>

0 commit comments

Comments
 (0)