SpUD-io 1.0.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.
- spud_io-1.0.0/LICENSE +21 -0
- spud_io-1.0.0/PKG-INFO +13 -0
- spud_io-1.0.0/README.md +0 -0
- spud_io-1.0.0/pyproject.toml +22 -0
- spud_io-1.0.0/setup.cfg +4 -0
- spud_io-1.0.0/src/SpUD_io/RFA.py +764 -0
- spud_io-1.0.0/src/SpUD_io/SpUD_upload.py +56 -0
- spud_io-1.0.0/src/SpUD_io/__init__.py +0 -0
- spud_io-1.0.0/src/SpUD_io.egg-info/PKG-INFO +13 -0
- spud_io-1.0.0/src/SpUD_io.egg-info/SOURCES.txt +11 -0
- spud_io-1.0.0/src/SpUD_io.egg-info/dependency_links.txt +1 -0
- spud_io-1.0.0/src/SpUD_io.egg-info/requires.txt +1 -0
- spud_io-1.0.0/src/SpUD_io.egg-info/top_level.txt +1 -0
spud_io-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 crandallowen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spud_io-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SpUD_io
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A package that interprets Spectrum XXI output files and uploads data to the NOAA Spectrum Usage Database application
|
|
5
|
+
Author-email: Owen Crandall <crandallowen@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: psycopg[c]
|
|
13
|
+
Dynamic: license-file
|
spud_io-1.0.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "SpUD_io"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Owen Crandall", email = "crandallowen@gmail.com"},
|
|
10
|
+
]
|
|
11
|
+
description = "A package that interprets Spectrum XXI output files and uploads data to the NOAA Spectrum Usage Database application"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"psycopg[c]",
|
|
22
|
+
]
|
spud_io-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
# RFA.py
|
|
2
|
+
# This file is a module that allows a user to more easily manipulate output data from SXXI. This module contains a class declaration
|
|
3
|
+
# for an RFA object, useful methods for manipulating RFA objects, a function to import records in either GMF or SFAF format,
|
|
4
|
+
# functions for exporting the data in particular formats, and functions for converting SXXI values into more readable formats.
|
|
5
|
+
# The current version of this module should be considered an 'alpha' version as it contains minimal documentation and error handling.
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import datetime
|
|
9
|
+
import re
|
|
10
|
+
from datetime import date
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
# An RFA object that represents a Radio Frequency Assignment from SXXI using fields derived from both GMF and SFAF format
|
|
14
|
+
# Some SXXI columns can be repeated to represent multiple values for a particular column which are represented as lists
|
|
15
|
+
# in this object declaration. Columns that can only ever have one value are represented as strings.
|
|
16
|
+
class RFA:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.serial_number = None
|
|
19
|
+
self.agency_action_number = None
|
|
20
|
+
self.classification = None
|
|
21
|
+
self.bureau = None
|
|
22
|
+
self.agency = None
|
|
23
|
+
self.record_type = None
|
|
24
|
+
self.main_function_id = None
|
|
25
|
+
self.intermediate_function_id = None
|
|
26
|
+
self.detailed_function_id = None
|
|
27
|
+
self.irac_docket_number = []
|
|
28
|
+
self.docket_number_old = None
|
|
29
|
+
self.sxxi_frequency = None
|
|
30
|
+
self.sxxi_frequency_upper_limit = None
|
|
31
|
+
self.sxxi_excluded_frequency_bands = []
|
|
32
|
+
self.sxxi_paired_frequency = []
|
|
33
|
+
self.gmf_time = None
|
|
34
|
+
self.irac_notes = []
|
|
35
|
+
self.free_text = []
|
|
36
|
+
self.misc_agency_data = []
|
|
37
|
+
self.fas_agenda = []
|
|
38
|
+
self.supplementary_details = ''
|
|
39
|
+
self.point_of_contact = None
|
|
40
|
+
self.poc_name = None
|
|
41
|
+
self.poc_phone_number = None
|
|
42
|
+
self.poc_verification_date = None
|
|
43
|
+
self.joint_agency_names = []
|
|
44
|
+
self.international_coordination_id = None
|
|
45
|
+
self.canadian_coordination_comments = []
|
|
46
|
+
self.mexican_coordination_comments = []
|
|
47
|
+
self.user_net_code = []
|
|
48
|
+
# Emission group
|
|
49
|
+
self.station_class = []
|
|
50
|
+
self.emission_designator = []
|
|
51
|
+
self.sxxi_power = []
|
|
52
|
+
# These two are not used in DoC records
|
|
53
|
+
self.effective_radiated_power = []
|
|
54
|
+
self.power_augmentation = []
|
|
55
|
+
# Transmitter group
|
|
56
|
+
self.tx_state_country_code = None
|
|
57
|
+
self.tx_antenna_location = None
|
|
58
|
+
self.tx_station_control = None
|
|
59
|
+
self.tx_station_call_sign = None
|
|
60
|
+
self.tx_antenna_latitude = None
|
|
61
|
+
self.tx_antenna_longitude = None
|
|
62
|
+
self.tx_authorized_radius = None
|
|
63
|
+
self.tx_inclination_angle = None
|
|
64
|
+
self.tx_apogee = None
|
|
65
|
+
self.tx_perigee = None
|
|
66
|
+
self.tx_period_of_orbit = None
|
|
67
|
+
self.tx_number_of_satellites = None
|
|
68
|
+
self.tx_power_density = None
|
|
69
|
+
self.tx_equipment_nomenclature = []
|
|
70
|
+
self.tx_system_name = None
|
|
71
|
+
self.tx_number_of_stations = None
|
|
72
|
+
self.tx_ots_equipment = None
|
|
73
|
+
self.tx_radar_tunability = None
|
|
74
|
+
self.tx_pulse_duration = []
|
|
75
|
+
self.tx_pulse_repetition_rate = []
|
|
76
|
+
self.tx_antenna_name = None
|
|
77
|
+
self.tx_antenna_nomenclature = None
|
|
78
|
+
self.tx_antenna_gain = []
|
|
79
|
+
self.tx_antenna_elevation = None
|
|
80
|
+
self.tx_antenna_feed_point_height = None
|
|
81
|
+
self.tx_antenna_horizontal_beamwidth = None
|
|
82
|
+
self.tx_antenna_azimuth = None
|
|
83
|
+
self.tx_antenna_orientation = None
|
|
84
|
+
self.tx_antenna_polarization = None
|
|
85
|
+
self.tx_jsc_area_code = None
|
|
86
|
+
# Receiver group
|
|
87
|
+
self.rx_state_country_code = []
|
|
88
|
+
self.rx_antenna_location = []
|
|
89
|
+
self.rx_control_id_and_server_system_id = []
|
|
90
|
+
self.rx_antenna_latitude = []
|
|
91
|
+
self.rx_antenna_longitude = []
|
|
92
|
+
self.rx_station_call_sign = []
|
|
93
|
+
self.rx_authorized_radius = []
|
|
94
|
+
self.rx_repeater_indicator = []
|
|
95
|
+
self.rx_inclination_angle = []
|
|
96
|
+
self.rx_apogee = []
|
|
97
|
+
self.rx_perigee = []
|
|
98
|
+
self.rx_period_of_orbit = []
|
|
99
|
+
self.rx_number_of_satellites = []
|
|
100
|
+
self.rx_equipment_nomenclature = []
|
|
101
|
+
self.rx_antenna_name = []
|
|
102
|
+
self.rx_antenna_nomenclature = []
|
|
103
|
+
self.rx_antenna_gain = []
|
|
104
|
+
self.rx_antenna_elevation = []
|
|
105
|
+
self.rx_antenna_feed_point_height = []
|
|
106
|
+
self.rx_antenna_horizontal_beamwidth = []
|
|
107
|
+
self.rx_antenna_azimuth = []
|
|
108
|
+
self.rx_antenna_orientation = []
|
|
109
|
+
self.rx_antenna_polarization = []
|
|
110
|
+
self.rx_jsc_area_code = []
|
|
111
|
+
# Area authorization
|
|
112
|
+
self.authorized_area_both = []
|
|
113
|
+
self.rx_authorized_area = []
|
|
114
|
+
self.tx_authorized_area = []
|
|
115
|
+
self.excepted_states_both = []
|
|
116
|
+
self.rx_excepted_states = []
|
|
117
|
+
self.tx_excepted_states = []
|
|
118
|
+
self.authorized_states_both = []
|
|
119
|
+
self.rx_authorized_states = []
|
|
120
|
+
self.tx_authorized_states = []
|
|
121
|
+
# Dates
|
|
122
|
+
self.last_transaction_date = None
|
|
123
|
+
self.revision_date = None
|
|
124
|
+
self.authorization_date = None
|
|
125
|
+
self.expiration_date = None
|
|
126
|
+
self.review_date = None
|
|
127
|
+
self.entry_date = None
|
|
128
|
+
self.receipt_date = None
|
|
129
|
+
# Likely not entered into database for now
|
|
130
|
+
# self.foi_exempt = None
|
|
131
|
+
# self.approval_authority = None
|
|
132
|
+
# self.data_source = None
|
|
133
|
+
# self.routine_agenda_item = None
|
|
134
|
+
# custom columns
|
|
135
|
+
self.power_w = []
|
|
136
|
+
self.center_frequency = None
|
|
137
|
+
self.max_power = None
|
|
138
|
+
self.frequency_band = None
|
|
139
|
+
self.bandwidth = None
|
|
140
|
+
self.tx_lat_long = None
|
|
141
|
+
self.rx_lat_long = []
|
|
142
|
+
|
|
143
|
+
# This method overrides the default str() function to print the RFA's serial number.
|
|
144
|
+
def __str__(self):
|
|
145
|
+
return f'<RFA: {self.serial_number}>'
|
|
146
|
+
|
|
147
|
+
def __getitem__(self, item):
|
|
148
|
+
return getattr(self, item)
|
|
149
|
+
|
|
150
|
+
# This method is used to convert an RFA object into a string in .csv format.
|
|
151
|
+
# Any fields that contain commas will have all commas converted into semicolons to be compatible with .csv format.
|
|
152
|
+
# Any list-type fields will be joined into a single field seperated by '|' characters.
|
|
153
|
+
# Any date-type fields will be output in mm/dd/yyyy format
|
|
154
|
+
def toCSVRow(self):
|
|
155
|
+
row = []
|
|
156
|
+
for value in self.__dict__.values():
|
|
157
|
+
if isinstance(value, list):
|
|
158
|
+
value = '|'.join(map(str, value))
|
|
159
|
+
elif isinstance(value, date):
|
|
160
|
+
value = value.strftime('%m/%d/%Y')
|
|
161
|
+
elif value is None:
|
|
162
|
+
value = ''
|
|
163
|
+
row.append(value.replace(',',';'))
|
|
164
|
+
return ','.join(row)
|
|
165
|
+
|
|
166
|
+
# def toCSVRow_formatted(self):
|
|
167
|
+
# row = []
|
|
168
|
+
# for key, value in self.__dict__.items():
|
|
169
|
+
# if key in ['frequency', 'frequency_upper_limit', 'excluded_frequency_bands', 'paired_frequency']:
|
|
170
|
+
# if isinstance(value, list):
|
|
171
|
+
# value = '|'.join(map(formatFrequency, value))
|
|
172
|
+
# else:
|
|
173
|
+
# value = formatFrequency(value)
|
|
174
|
+
# elif key == 'power':
|
|
175
|
+
# value = '|'.join(map(formatPower, value))
|
|
176
|
+
# elif isinstance(value, list):
|
|
177
|
+
# value = '|'.join(value)
|
|
178
|
+
# elif isinstance(value, date):
|
|
179
|
+
# value = value.strftime('%m/%d/%Y')
|
|
180
|
+
# elif value is None:
|
|
181
|
+
# value = ''
|
|
182
|
+
# row.append(value.replace(',',';'))
|
|
183
|
+
# return ','.join(row)
|
|
184
|
+
|
|
185
|
+
# This method is the same as toCSVRow(), but it only outputs specified fields.
|
|
186
|
+
def toCSVRow_NWSFormat(self):
|
|
187
|
+
main_function_id = self.main_function_id.replace(',', ';')
|
|
188
|
+
intermediate_function_id = self.intermediate_function_id.replace(',', ';')
|
|
189
|
+
detailed_function_id = self.detailed_function_id.replace(',', ';')
|
|
190
|
+
point_of_contact = self.point_of_contact.replace(',', ';')
|
|
191
|
+
revision_date = self.revision_date.strftime('%m/%d/%Y')
|
|
192
|
+
station_classes = '|'.join(self.station_class)
|
|
193
|
+
emission_designators = '|'.join(self.emission_designator)
|
|
194
|
+
powers = '|'.join(self.sxxi_power)
|
|
195
|
+
tx_antenna_name = '|'.join(self.tx_antenna_name)
|
|
196
|
+
tx_antenna_polarization = '|'.join(self.tx_antenna_polarization)
|
|
197
|
+
tx_antenna_orientation = '|'.join(self.tx_antenna_orientation)
|
|
198
|
+
return f'{self.serial_number},{self.agency_action_number},{main_function_id},{intermediate_function_id},{detailed_function_id},{self.sxxi_frequency},{point_of_contact},{revision_date},{self.tx_state_country_code},{self.tx_antenna_location},{self.tx_antenna_latitude},{self.tx_antenna_longitude},{station_classes},{emission_designators},{powers},{self.last_transaction_date},{self.record_type},{tx_antenna_name},{tx_antenna_polarization},{tx_antenna_orientation},{self.tx_station_call_sign}'
|
|
199
|
+
|
|
200
|
+
def toCSVRow_TrackerFormat(self):
|
|
201
|
+
point_of_contact = self.point_of_contact.replace(',', ';')
|
|
202
|
+
return f'{self.serial_number},{self.agency_action_number},{self.revision_date},{point_of_contact}'
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# This function expects a .txt file which was generated by SXXI using any of the SFAF 1 Column or GMF 1 Column output options
|
|
206
|
+
# and returns a list of RFA objects that correspond one-to-one with records found in the file. This function converts
|
|
207
|
+
# records from either format into a joined format. It should be noted that there are some columns in SFAF format that are
|
|
208
|
+
# not present in GMF format, so the resulting list of RFAs may have slightly different values depending on if an SFAF
|
|
209
|
+
# 1 Column or GMF 1 Column file was used.
|
|
210
|
+
# All column tags from GMF and SFAF format should be included, but this module was developed from an unclassifed release of SXXI
|
|
211
|
+
# with unclassified data, so classified fields will not be handled. Any unrecognized tags will be printed to the console
|
|
212
|
+
# along with the serial number of the record in which they were found.
|
|
213
|
+
# NOTE: If using an SFAF 1 Column output that only contains a subset of columns and not the full record, the '005' column which
|
|
214
|
+
# contains the record classification is required to detect the beginning of an new record. This will be fixed in later versions.
|
|
215
|
+
def importRFAsFrom1ColFile(filename):
|
|
216
|
+
with open(filename, 'r') as iFile:
|
|
217
|
+
lines = iFile.readlines()
|
|
218
|
+
mode = 'assignment'
|
|
219
|
+
if '_p_' in filename:
|
|
220
|
+
mode = 'proposal'
|
|
221
|
+
return importRFAs(lines, mode)
|
|
222
|
+
|
|
223
|
+
def importRFAs(lines, mode='assignment'):
|
|
224
|
+
# regular expression strings
|
|
225
|
+
receiver_group_tag = '(.*),R\d{2}$'
|
|
226
|
+
|
|
227
|
+
serial_suffix = ''
|
|
228
|
+
if mode == 'proposal':
|
|
229
|
+
serial_suffix = 'p'
|
|
230
|
+
|
|
231
|
+
RFAs = []
|
|
232
|
+
rfa = None
|
|
233
|
+
for line_no, line in enumerate(lines, start=1):
|
|
234
|
+
if line == '':
|
|
235
|
+
continue
|
|
236
|
+
# This is some error handling in the rare case that a record has a row with a tag but no value.
|
|
237
|
+
try:
|
|
238
|
+
tag, value = line.strip().split(maxsplit=1)
|
|
239
|
+
except ValueError as err:
|
|
240
|
+
# print(f'{type(err)}: {err}')
|
|
241
|
+
tag = line.strip()
|
|
242
|
+
value = ''
|
|
243
|
+
if tag != '115.':
|
|
244
|
+
if rfa is not None and rfa.serial_number is not None:
|
|
245
|
+
raise Exception(f'RFA with serial number {rfa.serial_number} has no value for tag {tag}') from None
|
|
246
|
+
else:
|
|
247
|
+
raise Exception(f'Line number {line_no} has no value for tag {tag}') from None
|
|
248
|
+
else:
|
|
249
|
+
continue
|
|
250
|
+
except Exception as err:
|
|
251
|
+
if rfa is not None and rfa.serial_number is not None:
|
|
252
|
+
print(f'Unexpected Error: {type(err)} on RFA with serial number {rfa.serial_number}', file=sys.stderr)
|
|
253
|
+
else:
|
|
254
|
+
print(f'Unexpected Error: {type(err)} on line number {line_no}', file=sys.stderr)
|
|
255
|
+
raise err
|
|
256
|
+
# Remove the ',R##' tags from values in receiver groups past the first. This only applies to files in SFAF format.
|
|
257
|
+
if matches := re.findall('(.*),R\d{2}$', value):
|
|
258
|
+
# print(f'{value} becomes {matches[0]}')
|
|
259
|
+
value = matches[0]
|
|
260
|
+
|
|
261
|
+
# If a value is '$', the loop will skip to the next iteration and not record the value. May need to be handled different at client request.
|
|
262
|
+
if value == '$':
|
|
263
|
+
continue
|
|
264
|
+
try:
|
|
265
|
+
# In these formats, there is no delimination between different records, so the end of a record is determined by the start
|
|
266
|
+
# of a new one. In GMF format, the first tag of a record is 'SER' which is the serial number, but in SFAF format, the
|
|
267
|
+
# first tag is '005' which is the security classification.
|
|
268
|
+
if tag == 'SER01' or tag == '005.': #SFAF uses 005 for FOI and CDD in addition to CLA. May need to be handled differently
|
|
269
|
+
if rfa != None:
|
|
270
|
+
addCustomColumns(rfa)
|
|
271
|
+
RFAs.append(rfa)
|
|
272
|
+
rfa = RFA()
|
|
273
|
+
if tag == 'SER01':
|
|
274
|
+
rfa.serial_number = value + serial_suffix
|
|
275
|
+
else:
|
|
276
|
+
rfa.classification = value
|
|
277
|
+
elif tag == '102.':
|
|
278
|
+
# print(value)
|
|
279
|
+
rfa.serial_number = value + serial_suffix
|
|
280
|
+
elif tag == 'TYP01' or tag == '010.':
|
|
281
|
+
rfa.record_type = value
|
|
282
|
+
elif tag == 'DAT01' or tag == '911.':
|
|
283
|
+
rfa.last_transaction_date = formatGMFDate(value) if tag == 'DAT01' else formatSFAFDate(value)
|
|
284
|
+
elif tag == 'CLA01':
|
|
285
|
+
rfa.classification = value
|
|
286
|
+
elif tag == 'FOI01':
|
|
287
|
+
# rfa.foi_exempt = value
|
|
288
|
+
continue
|
|
289
|
+
elif tag == 'ACN01' or tag == '956.':
|
|
290
|
+
rfa.agency_action_number = value
|
|
291
|
+
elif tag == 'EXD01' or tag == '141.':
|
|
292
|
+
rfa.expiration_date = formatGMFDate(value) if tag == 'EXD01' else formatSFAFDate(value)
|
|
293
|
+
elif tag == '142.':
|
|
294
|
+
rfa.review_date = formatSFAFDate(value)
|
|
295
|
+
elif tag == '144.':
|
|
296
|
+
# rfa.approval_authority = value
|
|
297
|
+
continue
|
|
298
|
+
elif tag == 'BUR01' or tag == '203.':
|
|
299
|
+
rfa.bureau = value
|
|
300
|
+
elif tag == '200.':
|
|
301
|
+
rfa.agency = value
|
|
302
|
+
elif tag[:3] == 'NET' or tag[:3] == '208':
|
|
303
|
+
rfa.user_net_code.append(value)
|
|
304
|
+
elif tag == 'FRQ01':
|
|
305
|
+
rfa.sxxi_frequency = value
|
|
306
|
+
elif tag == '110.':
|
|
307
|
+
if '-' in value:
|
|
308
|
+
unit = value[0]
|
|
309
|
+
lower, upper = value[1:].split('-')
|
|
310
|
+
rfa.sxxi_frequency = unit + lower
|
|
311
|
+
rfa.sxxi_frequency_upper_limit = unit + upper
|
|
312
|
+
else:
|
|
313
|
+
rfa.sxxi_frequency = value
|
|
314
|
+
elif tag[:3] == 'PRD' or tag[:3] == '506':
|
|
315
|
+
rfa.sxxi_paired_frequency.append(value)
|
|
316
|
+
elif tag == 'FRU01':
|
|
317
|
+
rfa.sxxi_frequency_upper_limit = value
|
|
318
|
+
elif tag[:3] == 'FBE' or tag[:3] == '111':
|
|
319
|
+
rfa.sxxi_excluded_frequency_bands.append(value)
|
|
320
|
+
elif tag[:3] == 'STC' or tag[:3] == '113':
|
|
321
|
+
rfa.station_class.append(value)
|
|
322
|
+
elif tag[:3] == 'EMS' or tag[:3] == '114':
|
|
323
|
+
rfa.emission_designator.append(value)
|
|
324
|
+
elif tag[:3] == 'PWR' or tag[:3] == '115':
|
|
325
|
+
rfa.sxxi_power.append(value)
|
|
326
|
+
elif tag[:3] == '117':
|
|
327
|
+
rfa.effective_radiated_power.append(value)
|
|
328
|
+
elif tag[:3] == '118':
|
|
329
|
+
rfa.power_augmentation.append(value)
|
|
330
|
+
elif tag == 'TME01' or tag == '130.':
|
|
331
|
+
rfa.gmf_time = value
|
|
332
|
+
elif tag == 'XSC01' or tag == '300.':
|
|
333
|
+
rfa.tx_state_country_code = value
|
|
334
|
+
elif tag == 'XAL01' or tag == '301.':
|
|
335
|
+
rfa.tx_antenna_location = value
|
|
336
|
+
elif tag == 'XLA01':
|
|
337
|
+
rfa.tx_antenna_latitude = value
|
|
338
|
+
elif tag == 'XLG01':
|
|
339
|
+
rfa.tx_antenna_longitude = value
|
|
340
|
+
elif tag == 'XRD01' or tag == '306.':
|
|
341
|
+
rfa.tx_authorized_radius = value
|
|
342
|
+
elif tag[:3] == 'ARB':
|
|
343
|
+
rfa.authorized_area_both.append(value)
|
|
344
|
+
elif tag == 'XAR01':
|
|
345
|
+
rfa.tx_authorized_area.append(value)
|
|
346
|
+
elif tag[:3] == '530':
|
|
347
|
+
if value[:3] == 'ART':
|
|
348
|
+
rfa.tx_authorized_area.append(value[4:])
|
|
349
|
+
elif value[:3] == 'ARR':
|
|
350
|
+
rfa.rx_authorized_area.append(value[4:])
|
|
351
|
+
elif value[:3] == 'ARB':
|
|
352
|
+
rfa.authorized_area_both.append(value[4:])
|
|
353
|
+
else:
|
|
354
|
+
print(f'Unknown tag in SFAF 530 of record{rfa.serial_number}')
|
|
355
|
+
elif tag[:3] == 'EQS' or tag[:3] == '344':
|
|
356
|
+
rfa.tx_ots_equipment = value
|
|
357
|
+
elif tag == '303.':
|
|
358
|
+
rfa.tx_antenna_latitude = value[:7] #double check if char count is sufficient condition
|
|
359
|
+
rfa.tx_antenna_longitude = value[7:]
|
|
360
|
+
elif tag[:3] == 'XSE' or tag[:3] == '358':
|
|
361
|
+
rfa.tx_antenna_elevation = value
|
|
362
|
+
elif tag[:3] == 'XAH' or tag[:3] == '359':
|
|
363
|
+
rfa.tx_antenna_feed_point_height = value
|
|
364
|
+
elif tag == 'XRC01' or tag == '302.':
|
|
365
|
+
rfa.tx_station_control = value
|
|
366
|
+
elif tag == 'XCL01' or tag == '304.':
|
|
367
|
+
rfa.tx_station_call_sign = value
|
|
368
|
+
elif tag[:3] == 'XAG' or tag[:3] == '357':
|
|
369
|
+
rfa.tx_antenna_gain.append(value)
|
|
370
|
+
elif tag[:3] == 'XAT' or tag[:3] == '354':
|
|
371
|
+
rfa.tx_antenna_name = value
|
|
372
|
+
elif tag[:3] == 'XAK' or tag[:3] == '355':
|
|
373
|
+
rfa.tx_antenna_nomenclature = value
|
|
374
|
+
elif tag[:3] == 'XAZ':
|
|
375
|
+
rfa.tx_antenna_orientation = value
|
|
376
|
+
elif tag[:3] == '362': #Will need additional processing of tag 362
|
|
377
|
+
if ',' in value:
|
|
378
|
+
rfa.tx_antenna_orientation, rfa.tx_antenna_azimuth = value.split(',')
|
|
379
|
+
else:
|
|
380
|
+
rfa.tx_antenna_orientation = value
|
|
381
|
+
elif tag[:3] == 'XAA':
|
|
382
|
+
rfa.tx_antenna_azimuth = value
|
|
383
|
+
elif tag[:3] == 'XAP' or tag[:3] == '363':
|
|
384
|
+
rfa.tx_antenna_polarization = value
|
|
385
|
+
elif tag == 'TUN01' or tag == '345.':
|
|
386
|
+
rfa.tx_radar_tunability = value
|
|
387
|
+
elif tag[:3] == 'PDD' or tag[:3] == '346':
|
|
388
|
+
rfa.tx_pulse_duration.append(value)
|
|
389
|
+
elif tag[:3] == 'PRR' or tag[:3] == '347':
|
|
390
|
+
rfa.tx_pulse_repetition_rate.append(value)
|
|
391
|
+
elif tag[:3] == 'XEQ' or tag[:3] == '340':
|
|
392
|
+
rfa.tx_equipment_nomenclature.append(value)
|
|
393
|
+
elif tag[:3] == 'NTT':
|
|
394
|
+
rfa.tx_number_of_stations = value
|
|
395
|
+
elif tag[:3] == 'NAM':
|
|
396
|
+
rfa.tx_system_name = value
|
|
397
|
+
elif tag[:3] == '341':
|
|
398
|
+
rfa.tx_number_of_stations, rfa.tx_system_name = value.split(',')
|
|
399
|
+
if rfa.tx_number_of_stations[0] == 'X':
|
|
400
|
+
rfa.tx_number_of_stations = None
|
|
401
|
+
elif tag[:3] == '373':
|
|
402
|
+
rfa.tx_jsc_area_code == value
|
|
403
|
+
elif tag[:3] == 'RSC' or tag[:3] == '400':
|
|
404
|
+
rfa.rx_state_country_code.append(value)
|
|
405
|
+
elif tag[:3] == 'RAL' or tag[:3] == '401':
|
|
406
|
+
rfa.rx_antenna_location.append(value)
|
|
407
|
+
elif tag[:3] == 'RLA':
|
|
408
|
+
rfa.rx_antenna_latitude.append(value)
|
|
409
|
+
elif tag[:3] == 'RLG':
|
|
410
|
+
rfa.rx_antenna_longitude.append(value)
|
|
411
|
+
elif tag[:3] == '403':
|
|
412
|
+
rfa.rx_antenna_latitude.append(value[:7])
|
|
413
|
+
rfa.rx_antenna_longitude.append(value[7:])
|
|
414
|
+
elif tag[:3] == 'RSE' or tag[:3] == '458':
|
|
415
|
+
rfa.rx_antenna_elevation.append(value)
|
|
416
|
+
elif tag[:3] == 'RRD' or tag[:3] == '406':
|
|
417
|
+
rfa.rx_authorized_radius.append(value)
|
|
418
|
+
elif tag[:3] == 'RRC' or tag[:3] == '402':
|
|
419
|
+
rfa.rx_control_id_and_server_system_id.append(value)
|
|
420
|
+
elif tag[:3] == 'RCL' or tag[:3] == '404':
|
|
421
|
+
rfa.rx_station_call_sign.append(value)
|
|
422
|
+
elif tag[:3] == 'RAG' or tag[:3] == '457':
|
|
423
|
+
rfa.rx_antenna_gain.append(value)
|
|
424
|
+
elif tag[:3] == 'RAT' or tag[:3] == '454':
|
|
425
|
+
rfa.rx_antenna_name.append(value)
|
|
426
|
+
elif tag[:3] == 'RAK' or tag[:3] == '455':
|
|
427
|
+
rfa.rx_antenna_nomenclature.append(value)
|
|
428
|
+
elif tag[:3] == 'RAH' or tag[:3] == '459':
|
|
429
|
+
rfa.rx_antenna_feed_point_height.append(value)
|
|
430
|
+
elif tag[:3] == 'RAZ' or tag[:3] == '462':
|
|
431
|
+
rfa.rx_antenna_orientation.append(value)
|
|
432
|
+
elif tag[:3] == 'RAA':
|
|
433
|
+
rfa.rx_antenna_azimuth.append(value)
|
|
434
|
+
elif tag[:3] == 'RAZ':
|
|
435
|
+
rfa.rx_antenna_orientation.append(value)
|
|
436
|
+
elif tag[:3] == '462':
|
|
437
|
+
if value.find(',') == -1:
|
|
438
|
+
rfa.rx_antenna_orientation.append(value)
|
|
439
|
+
else:
|
|
440
|
+
rfa.rx_antenna_orientation.append(value[:value.find(',')])
|
|
441
|
+
elif tag[:3] == 'RAP' or tag[:3] == '463':
|
|
442
|
+
rfa.rx_antenna_polarization.append(value)
|
|
443
|
+
elif tag[:3] == 'RPT' or tag[:3] == '408':
|
|
444
|
+
rfa.rx_repeater_indicator.append(value)
|
|
445
|
+
elif tag[:3] == 'REQ' or tag[:3] == '440':
|
|
446
|
+
rfa.rx_equipment_nomenclature.append(value)
|
|
447
|
+
elif tag[:3] == '473':
|
|
448
|
+
rfa.rx_jsc_area_code.append(value[0])
|
|
449
|
+
elif tag == 'SPD01' or tag == '321.':
|
|
450
|
+
rfa.tx_power_density = value
|
|
451
|
+
elif tag[:3] == 'XBW' or tag[:3] == '360':
|
|
452
|
+
rfa.tx_antenna_horizontal_beamwidth =value
|
|
453
|
+
elif tag == 'XIN01' or tag == '315.':
|
|
454
|
+
rfa.tx_inclination_angle = value
|
|
455
|
+
elif tag == 'XAE01' or tag == '316.':
|
|
456
|
+
rfa.tx_apogee = value
|
|
457
|
+
elif tag == 'XPE01' or tag == '317.':
|
|
458
|
+
rfa.tx_perigee = value
|
|
459
|
+
elif tag == 'XPD01' or tag == '318.':
|
|
460
|
+
rfa.tx_period_of_orbit = value
|
|
461
|
+
elif tag == 'XNR01' or tag == '319.':
|
|
462
|
+
rfa.tx_number_of_satellites = value
|
|
463
|
+
elif tag[:3] == 'RBW' or tag[:3] == '460':
|
|
464
|
+
rfa.rx_antenna_horizontal_beamwidth.append(value)
|
|
465
|
+
elif tag[:3] == 'RIN' or tag[:3] == '415':
|
|
466
|
+
rfa.rx_inclination_angle.append(value)
|
|
467
|
+
elif tag[:3] == 'RAE' or tag[:3] == '416':
|
|
468
|
+
rfa.rx_apogee.append(value)
|
|
469
|
+
elif tag[:3] == 'RPE' or tag[:3] == '417':
|
|
470
|
+
rfa.rx_perigee.append(value)
|
|
471
|
+
elif tag[:3] == 'RPD' or tag[:3] == '418':
|
|
472
|
+
rfa.rx_period_of_orbit.append(value)
|
|
473
|
+
elif tag[:3] == 'RNR' or tag[:3] == '419':
|
|
474
|
+
rfa.rx_number_of_satellites.append(value)
|
|
475
|
+
elif tag[:3] == 'JNT' or tag[:3] == '147':
|
|
476
|
+
rfa.joint_agency_names.append(value)
|
|
477
|
+
elif tag == 'MFI01' or tag == '511.':
|
|
478
|
+
rfa.main_function_id = value
|
|
479
|
+
elif tag == 'IFI01' or tag == '512.':
|
|
480
|
+
rfa.intermediate_function_id = value
|
|
481
|
+
elif tag[:3] == 'DFI' or tag[:3] == '513':
|
|
482
|
+
rfa.detailed_function_id = value
|
|
483
|
+
elif tag[:3] == 'NTS' or tag[:3] == '500':
|
|
484
|
+
rfa.irac_notes.append(value)
|
|
485
|
+
elif tag[:3] == '117':
|
|
486
|
+
rfa.effective_radiated_power.append(value)
|
|
487
|
+
elif tag == 'ICI01' or tag == '151.':
|
|
488
|
+
rfa.international_coordination_id = value
|
|
489
|
+
elif tag[:3] == 'CAN':
|
|
490
|
+
rfa.canadian_coordination_comments.append(value)
|
|
491
|
+
elif tag[:3] == 'MEX':
|
|
492
|
+
rfa.mexican_coordination_comments.append(value)
|
|
493
|
+
elif tag[:3] == '152':
|
|
494
|
+
if value[0] == 'C':
|
|
495
|
+
rfa.canadian_coordination_comments.append(value[2:])
|
|
496
|
+
elif value[0] == 'M':
|
|
497
|
+
rfa.mexican_coordination_comments.append(value[2:])
|
|
498
|
+
else:
|
|
499
|
+
print(f'Unrecognized format for SFAF 152 in record {rfa.serial_number}')
|
|
500
|
+
elif tag[:3] == 'NOT' or tag[:3] == '501':
|
|
501
|
+
rfa.free_text.append(value)
|
|
502
|
+
elif tag == 'DOC01' or tag == '108.':
|
|
503
|
+
rfa.docket_number_old = value
|
|
504
|
+
elif tag == 'POC01' or tag == '803.':
|
|
505
|
+
rfa.point_of_contact = value
|
|
506
|
+
rfa.poc_name, rfa.poc_phone_number, rfa.poc_verification_date = rfa.point_of_contact.split(',')
|
|
507
|
+
rfa.poc_verification_date = formatGMFDate(rfa.poc_verification_date)
|
|
508
|
+
elif tag[:3] == 'AGN' or tag[:3] == '503':
|
|
509
|
+
rfa.misc_agency_data.append(value)
|
|
510
|
+
elif tag[:3] == 'FAS' or tag[:3] == '504':
|
|
511
|
+
rfa.fas_agenda.append(value)
|
|
512
|
+
elif tag == 'RTN01' or tag == '958.':
|
|
513
|
+
# rfa.routine_agenda_item = value
|
|
514
|
+
continue
|
|
515
|
+
elif tag[:3] == 'SUP' or tag[:3] == '520':
|
|
516
|
+
if rfa.supplementary_details != '':
|
|
517
|
+
rfa.supplementary_details += f' {value}'
|
|
518
|
+
else:
|
|
519
|
+
rfa.supplementary_details = value
|
|
520
|
+
elif tag[:3] == 'AUS' or tag[:3] == '103':
|
|
521
|
+
rfa.irac_docket_number.append(value)
|
|
522
|
+
elif tag == 'AUD01' or tag == '107.':
|
|
523
|
+
rfa.authorization_date = formatGMFDate(value) if tag == 'AUD01' else formatSFAFDate(value)
|
|
524
|
+
elif tag == 'RVD01' or tag == '143.':
|
|
525
|
+
rfa.revision_date = formatGMFDate(value) if tag == 'RVD01' else formatSFAFDate(value)
|
|
526
|
+
elif tag[:3] == 'AST' or tag[:3] == '531':
|
|
527
|
+
if value[:3] == 'ESB':
|
|
528
|
+
rfa.excepted_states_both.append(value[4:])
|
|
529
|
+
elif value[:3] == 'ESR':
|
|
530
|
+
rfa.rx_excepted_states.append(value[4:])
|
|
531
|
+
elif value[:3] == 'EST':
|
|
532
|
+
rfa.tx_excepted_states.append(value[4:])
|
|
533
|
+
elif value[:3] == 'LSB':
|
|
534
|
+
rfa.authorized_states_both.append(value[4:])
|
|
535
|
+
elif value[:3] == 'LSR':
|
|
536
|
+
rfa.rx_authorized_states.append(value[4:])
|
|
537
|
+
elif value[:3] == 'LST':
|
|
538
|
+
rfa.tx_authorized_states.append(value[4:])
|
|
539
|
+
else:
|
|
540
|
+
print(f'Unrecognized format for SFAF 531 or GMF AST in record {rfa.serial_number}')
|
|
541
|
+
elif tag == '924.':
|
|
542
|
+
# rfa.data_source = value
|
|
543
|
+
continue
|
|
544
|
+
elif tag == '927.':
|
|
545
|
+
rfa.entry_date = formatSFAFDate(value)
|
|
546
|
+
elif tag == '928.':
|
|
547
|
+
rfa.receipt_date = formatSFAFDate(value)
|
|
548
|
+
else:
|
|
549
|
+
print(f'Unknown Tag {tag} at line {line_no}')
|
|
550
|
+
except Exception as error:
|
|
551
|
+
if rfa is not None and rfa.serial_number is not None:
|
|
552
|
+
print(f'Unexpected Error: {type(error)} on RFA with serial number {rfa.serial_number}', file=sys.stderr)
|
|
553
|
+
else:
|
|
554
|
+
print(f'Unexpected Error: {type(error)} in line {line_no}', file=sys.stderr)
|
|
555
|
+
raise error
|
|
556
|
+
addCustomColumns(rfa)
|
|
557
|
+
RFAs.append(rfa)
|
|
558
|
+
return RFAs
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# This function takes a list of RFAs and a string that represents the name of a .csv file, and converts each RFA
|
|
562
|
+
# into .csv format, and exports them into a new file where each record is on its own line. The first line of
|
|
563
|
+
# the file is a header.
|
|
564
|
+
# This function includes file creation and writing which will require additional error handling to ensure proper
|
|
565
|
+
# resource handling and protection. This will be included in future versions.
|
|
566
|
+
def exportRFAsToCSV(RFAs, filename='output.csv'):
|
|
567
|
+
if os.path.isdir('./outputs/'):
|
|
568
|
+
filename = './outputs/' + filename
|
|
569
|
+
with open(filename, 'w') as oFile:
|
|
570
|
+
headers = ','.join(RFAs[0].__dict__.keys())
|
|
571
|
+
oFile.write(f'{headers}\n')
|
|
572
|
+
for rfa in RFAs:
|
|
573
|
+
oFile.write(f'{rfa.toCSVRow()}\n')
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def exportRFAsToCSV_formatted(RFAs, filename='output.csv'):
|
|
577
|
+
if os.path.isdir('./outputs/'):
|
|
578
|
+
filename = './outputs/' + filename
|
|
579
|
+
with open(filename, 'w') as oFile:
|
|
580
|
+
headers = ','.join(RFAs[0].__dict__.keys())
|
|
581
|
+
oFile.write(f'{headers}\n')
|
|
582
|
+
for rfa in RFAs:
|
|
583
|
+
oFile.write(f'{rfa.toCSVRow_formatted()}\n')
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# This function is the same as exportRFAsToCSV(), but uses the NWS format.
|
|
587
|
+
def exportRFAsToCSV_NWSFormat(RFAs, filename='output.csv'):
|
|
588
|
+
if os.path.isdir('./outputs/'):
|
|
589
|
+
filename = './outputs/' + filename
|
|
590
|
+
with open(filename, 'w') as oFile:
|
|
591
|
+
oFile.write('Serial Number,Action Number,Main Function Identifier,Intermediate Function Identifier,Detailed Function Identifier,Frequency,Point of Contact,Revision Date,Transmitter State/Country Code,Transmitter Antenna Location,Transmitter Latitude,Transmitter Longitude,Station Class(es),Emission Designator(s),Power(s),Last Transaction Date,Type of Action,Transmitter Antenna Name,Transmitter Antenna Polarization,Transmitter Antenna Orientation,Station Call Sign\n')
|
|
592
|
+
for rfa in RFAs:
|
|
593
|
+
oFile.write(f'{rfa.toCSVRow_NWSFormat()}\n')
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def exportRFAsToCSV_TrackerFormat(RFAs, filename='output.csv'):
|
|
597
|
+
if os.path.isdir('./outputs/'):
|
|
598
|
+
filename = './outputs/' + filename
|
|
599
|
+
with open(filename, 'w') as oFile:
|
|
600
|
+
oFile.write('Serial Number,Action Number,Revision Date,Point of Contact\n')
|
|
601
|
+
for rfa in RFAs:
|
|
602
|
+
oFile.write(f'{rfa.toCSVRow_TrackerFormat()}\n')
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# This function converts a frequency in SXXI format into kHz. The purpose of this function is to create a sortable
|
|
606
|
+
# format for frequencies.
|
|
607
|
+
# Future versions will include formatting options.
|
|
608
|
+
def formatFrequency(SXXI_frequency):
|
|
609
|
+
unit = SXXI_frequency[0].lower()
|
|
610
|
+
quantity = SXXI_frequency[1:]
|
|
611
|
+
|
|
612
|
+
if unit == 'h':
|
|
613
|
+
conversion = 0
|
|
614
|
+
elif unit == 'k':
|
|
615
|
+
conversion = 3
|
|
616
|
+
elif unit == 'm':
|
|
617
|
+
conversion = 6
|
|
618
|
+
elif unit == 'g':
|
|
619
|
+
conversion = 9
|
|
620
|
+
elif unit == 't':
|
|
621
|
+
conversion = 12
|
|
622
|
+
else:
|
|
623
|
+
print(f'Unknown unit \'{unit}\'in frequency format')
|
|
624
|
+
|
|
625
|
+
return str(float(quantity) * (10 ** conversion))
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def formatFrequencyBand(SXXI_frequency_band):
|
|
629
|
+
unit = SXXI_frequency_band[0].lower()
|
|
630
|
+
lower_end, upper_end = map(formatFrequency, list(map(lambda x: unit + x, SXXI_frequency_band[1:].split('-'))))
|
|
631
|
+
return (lower_end, upper_end)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
# This function converts a date in GMF format into standard dd/mm/yyyy format.
|
|
635
|
+
def formatGMFDate(GMF_date):
|
|
636
|
+
year = GMF_date[:2]
|
|
637
|
+
month = GMF_date[2:4]
|
|
638
|
+
day = GMF_date[4:]
|
|
639
|
+
|
|
640
|
+
if int(year) > 60:
|
|
641
|
+
year = '19' + year
|
|
642
|
+
else:
|
|
643
|
+
year = '20' + year
|
|
644
|
+
|
|
645
|
+
return datetime.date(int(year), int(month), int(day))
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
# This function converts a date in SFAF format into standard dd/mm/yyyy format.
|
|
649
|
+
def formatSFAFDate(SFAF_date):
|
|
650
|
+
year = SFAF_date[:4]
|
|
651
|
+
month = SFAF_date[4:6]
|
|
652
|
+
day = SFAF_date[6:]
|
|
653
|
+
return datetime.date(int(year), int(month), int(day))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# This function converts a power in SXXI format into Watts.
|
|
657
|
+
def formatPower(SXXI_power):
|
|
658
|
+
if SXXI_power is None or SXXI_power == '':
|
|
659
|
+
return 0
|
|
660
|
+
unit = SXXI_power[0]
|
|
661
|
+
quantity = SXXI_power[1:]
|
|
662
|
+
|
|
663
|
+
if unit == 'W':
|
|
664
|
+
conversion = 0
|
|
665
|
+
elif unit == 'K':
|
|
666
|
+
conversion = 3
|
|
667
|
+
elif unit == 'M':
|
|
668
|
+
conversion = 6
|
|
669
|
+
|
|
670
|
+
return float(quantity) * (10 ** conversion)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def formatLatLong(SXXI_latitude, SXXI_longitude):
|
|
674
|
+
lat_degrees, lat_minutes, lat_seconds, lat_sign = [(SXXI_latitude[i:i+2]) for i in range(0, len(SXXI_latitude), 2)]
|
|
675
|
+
long_degrees, long_minutes, long_seconds, long_sign = SXXI_longitude[:3], SXXI_longitude[3:5], SXXI_longitude[5:7], SXXI_longitude[7]
|
|
676
|
+
|
|
677
|
+
if lat_sign == 'N':
|
|
678
|
+
lat_sign = 1
|
|
679
|
+
else:
|
|
680
|
+
lat_sign = -1
|
|
681
|
+
|
|
682
|
+
if long_sign == 'E':
|
|
683
|
+
long_sign = 1
|
|
684
|
+
else:
|
|
685
|
+
long_sign = -1
|
|
686
|
+
|
|
687
|
+
lat_degrees = lat_sign * DMSToDD(lat_degrees, lat_minutes, lat_seconds)
|
|
688
|
+
long_degrees = long_sign * DMSToDD(long_degrees, long_minutes, long_seconds)
|
|
689
|
+
|
|
690
|
+
return str(lat_degrees) + ',' + str(long_degrees)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def DMSToDD(degrees, minutes, seconds):
|
|
694
|
+
return float(degrees) + (float(minutes) / 60) + (float(seconds) / 3600)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def decodeEmissionDesignator(emissionDesignator):
|
|
698
|
+
before_point = ''
|
|
699
|
+
after_point = ''
|
|
700
|
+
unit = None
|
|
701
|
+
for char in emissionDesignator:
|
|
702
|
+
if unit is None:
|
|
703
|
+
if char.isnumeric():
|
|
704
|
+
before_point += char
|
|
705
|
+
else:
|
|
706
|
+
unit = char
|
|
707
|
+
else:
|
|
708
|
+
if char.isnumeric():
|
|
709
|
+
after_point += char
|
|
710
|
+
else:
|
|
711
|
+
break
|
|
712
|
+
if unit == 'N':
|
|
713
|
+
return None
|
|
714
|
+
else:
|
|
715
|
+
unit = unit.lower()
|
|
716
|
+
if after_point == '':
|
|
717
|
+
value = float(before_point)
|
|
718
|
+
else:
|
|
719
|
+
value = float(before_point + '.' + after_point)
|
|
720
|
+
if unit == 'h':
|
|
721
|
+
return value
|
|
722
|
+
elif unit == 'k':
|
|
723
|
+
return value * (10 ** 3)
|
|
724
|
+
elif unit == 'm':
|
|
725
|
+
return value * (10 ** 6)
|
|
726
|
+
elif unit == 'g':
|
|
727
|
+
return value * (10 ** 9)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def addCustomColumns(rfa):
|
|
731
|
+
for power in rfa.sxxi_power:
|
|
732
|
+
rfa.power_w.append(formatPower(power))
|
|
733
|
+
|
|
734
|
+
max_power = 0
|
|
735
|
+
for power in rfa.power_w:
|
|
736
|
+
power = float(power)
|
|
737
|
+
if power > max_power:
|
|
738
|
+
max_power = power
|
|
739
|
+
rfa.max_power = str(max_power)
|
|
740
|
+
|
|
741
|
+
bandwidth = 0
|
|
742
|
+
for emission_designator in rfa.emission_designator:
|
|
743
|
+
band = decodeEmissionDesignator(emission_designator)
|
|
744
|
+
if band is not None and band > bandwidth:
|
|
745
|
+
bandwidth = band
|
|
746
|
+
rfa.bandwidth = str(bandwidth)
|
|
747
|
+
|
|
748
|
+
if rfa.sxxi_frequency_upper_limit is not None:
|
|
749
|
+
lower_limit = float(formatFrequency(rfa.sxxi_frequency))
|
|
750
|
+
upper_limit = float(formatFrequency(rfa.sxxi_frequency_upper_limit))
|
|
751
|
+
rfa.frequency_band = '['+str(lower_limit) + ',' + str(upper_limit)+']'
|
|
752
|
+
rfa.center_frequency = str(lower_limit + ((upper_limit - lower_limit) / 2))
|
|
753
|
+
else:
|
|
754
|
+
rfa.center_frequency = formatFrequency(rfa.sxxi_frequency)
|
|
755
|
+
rfa.frequency_band = '['+str(float(rfa.center_frequency) - (bandwidth / 2)) + ',' + str(float(rfa.center_frequency) + (bandwidth / 2))+']'
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
if rfa.tx_antenna_latitude is not None and rfa.tx_antenna_longitude is not None:
|
|
760
|
+
rfa.tx_lat_long = formatLatLong(rfa.tx_antenna_latitude, rfa.tx_antenna_longitude)
|
|
761
|
+
|
|
762
|
+
for rx_lat, rx_long in zip(rfa.rx_antenna_latitude, rfa.rx_antenna_longitude):
|
|
763
|
+
# print(rx_lat, rx_long)
|
|
764
|
+
rfa.rx_lat_long.append(formatLatLong(rx_lat, rx_long))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import psycopg
|
|
2
|
+
from psycopg import Rollback, sql
|
|
3
|
+
from SpUD import RFA
|
|
4
|
+
|
|
5
|
+
def progress_bar(current, total, bar_length=20):
|
|
6
|
+
fraction = current / total
|
|
7
|
+
|
|
8
|
+
arrow = int(fraction * bar_length - 1) * '-' + '>'
|
|
9
|
+
padding = int(bar_length - len(arrow)) * ' '
|
|
10
|
+
|
|
11
|
+
ending = '\n' if current == total else '\r'
|
|
12
|
+
|
|
13
|
+
print(f'Progress: [{arrow}{padding}] {int(fraction*100)}%', end=ending)
|
|
14
|
+
|
|
15
|
+
def connect(params):
|
|
16
|
+
connection = None
|
|
17
|
+
print('Connecting to the PostgreSQL database {0} as user {1}...'.format(params['dbname'], params['user']))
|
|
18
|
+
try:
|
|
19
|
+
connection = psycopg.connect(**params)
|
|
20
|
+
cursor = connection.cursor()
|
|
21
|
+
with connection.transaction():
|
|
22
|
+
cursor.execute('SELECT version()')
|
|
23
|
+
db_version = cursor.fetchone()
|
|
24
|
+
print(db_version)
|
|
25
|
+
return connection
|
|
26
|
+
except (Exception, psycopg.DatabaseError) as error:
|
|
27
|
+
print(f'Connection Error: {error}')
|
|
28
|
+
raise error
|
|
29
|
+
|
|
30
|
+
def disconnect(connection):
|
|
31
|
+
connection.close()
|
|
32
|
+
print('Database connection closed.')
|
|
33
|
+
|
|
34
|
+
def uploadRFAs(RFAs, connectParams, enableProgressBar=False):
|
|
35
|
+
current = 0
|
|
36
|
+
total = len(RFAs)
|
|
37
|
+
if enableProgressBar:
|
|
38
|
+
progress_bar(current, total)
|
|
39
|
+
columns = RFAs[0].__dict__.keys()
|
|
40
|
+
insertSQL = sql.SQL('INSERT INTO RFAs ({}) VALUES ({})').format(sql.SQL(', ').join(map(sql.Identifier, columns)), sql.SQL(', ').join(map(sql.Placeholder, columns)))
|
|
41
|
+
connection = connect(connectParams)
|
|
42
|
+
cursor = connection.cursor()
|
|
43
|
+
with connection.transaction():
|
|
44
|
+
cursor.execute('DELETE FROM RFAs')
|
|
45
|
+
for rfa in RFAs:
|
|
46
|
+
try:
|
|
47
|
+
cursor.execute(insertSQL, vars(rfa))
|
|
48
|
+
except Exception as error:
|
|
49
|
+
print(f'Execute Error: {error}')
|
|
50
|
+
print(f'Error on RFA with serial number: {rfa.serial_number}')
|
|
51
|
+
raise Rollback()
|
|
52
|
+
if enableProgressBar:
|
|
53
|
+
current += 1
|
|
54
|
+
progress_bar(current, total)
|
|
55
|
+
if connection is not None:
|
|
56
|
+
disconnect(connection)
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: SpUD_io
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A package that interprets Spectrum XXI output files and uploads data to the NOAA Spectrum Usage Database application
|
|
5
|
+
Author-email: Owen Crandall <crandallowen@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: psycopg[c]
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/SpUD_io/RFA.py
|
|
5
|
+
src/SpUD_io/SpUD_upload.py
|
|
6
|
+
src/SpUD_io/__init__.py
|
|
7
|
+
src/SpUD_io.egg-info/PKG-INFO
|
|
8
|
+
src/SpUD_io.egg-info/SOURCES.txt
|
|
9
|
+
src/SpUD_io.egg-info/dependency_links.txt
|
|
10
|
+
src/SpUD_io.egg-info/requires.txt
|
|
11
|
+
src/SpUD_io.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psycopg[c]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SpUD_io
|