satnogs-webscraper 1.4.2__tar.gz → 1.4.4__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.
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/PKG-INFO +9 -4
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/constants.py +2 -2
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/image_utils.py +11 -5
- satnogs_webscraper-1.4.4/satnogs_webscraper/observation_dataset.py +115 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_scraper.py +21 -19
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/request_utils.py +5 -2
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/scraper.py +8 -2
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/PKG-INFO +10 -5
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/SOURCES.txt +8 -2
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/setup.cfg +1 -1
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/setup.py +1 -1
- satnogs_webscraper-1.4.4/tests/test_constants.py +16 -0
- satnogs_webscraper-1.4.4/tests/test_image_utils.py +69 -0
- satnogs_webscraper-1.4.4/tests/test_observation_dataset.py +64 -0
- satnogs_webscraper-1.4.4/tests/test_observation_list_scraper.py +122 -0
- satnogs_webscraper-1.4.4/tests/test_observation_scraper.py +197 -0
- satnogs_webscraper-1.4.4/tests/test_request_utils.py +73 -0
- satnogs_webscraper-1.4.4/tests/test_scraper.py +136 -0
- satnogs_webscraper-1.4.2/README.rst +0 -0
- satnogs_webscraper-1.4.2/satnogs_webscraper/observation_dataset.py +0 -20
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/LICENSE.rst +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/README.md +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/__init__.py +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/mainCLI.py +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_list_scraper.py +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/progress_utils.py +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/dependency_links.txt +0 -0
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/requires.txt +4 -4
- {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/top_level.txt +0 -0
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: satnogs_webscraper
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: Scrape SATNOGS data into a pandas dataframe
|
|
5
|
-
Home-page: UNKNOWN
|
|
6
5
|
Author: Ada L
|
|
7
6
|
Author-email: the.nostra.tymus@gmail.com
|
|
8
7
|
License: MIT
|
|
9
8
|
Keywords: satellite satnogs
|
|
10
|
-
Platform: UNKNOWN
|
|
11
9
|
Classifier: Development Status :: 3 - Alpha
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
13
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
12
|
Classifier: Programming Language :: Python :: 3
|
|
15
13
|
Description-Content-Type: text/markdown
|
|
16
14
|
License-File: LICENSE.rst
|
|
15
|
+
Requires-Dist: pillow
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: bs4
|
|
19
|
+
Requires-Dist: html5lib
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Requires-Dist: dask[complete]
|
|
22
|
+
Requires-Dist: pytest
|
|
17
23
|
|
|
18
24
|
A webscraper to pull multiple satnogs observations into a single pandas dataframe
|
|
19
|
-
|
|
@@ -6,18 +6,18 @@ web_address = "https://network.satnogs.org/"
|
|
|
6
6
|
|
|
7
7
|
observation_template = {
|
|
8
8
|
'Observation_id': None,
|
|
9
|
+
'Timeframe': None,
|
|
9
10
|
'Satellite': None,
|
|
10
11
|
'Station': None,
|
|
11
12
|
'Status': None,
|
|
12
13
|
'Status_Message': None,
|
|
13
|
-
'Transmitter': None,
|
|
14
14
|
'Frequency': None,
|
|
15
15
|
'Mode': None,
|
|
16
16
|
'Metadata': None,
|
|
17
17
|
'Downloads': None,
|
|
18
18
|
'Waterfall_Status': None,
|
|
19
19
|
'Polar_Plot': None,
|
|
20
|
-
'demods':None
|
|
20
|
+
'demods': None
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
directories = dict()
|
|
@@ -73,12 +73,18 @@ def crop_and_save_psd(input_image, delete_original=True, greyscale=True, resize=
|
|
|
73
73
|
im_cropped = im_cropped.resize(resize_dimen, Image.Resampling.LANCZOS)
|
|
74
74
|
|
|
75
75
|
# Convert to greyscale and save as unit8 bytes to disk, using the original file name, minus the file extension
|
|
76
|
-
numpy_im = np.array(im_cropped)
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
new_file_name = "".join(input_image.split(".")[0:-1])
|
|
78
|
+
new_file_name += ".bin" if greyscale else ".png"
|
|
79
|
+
|
|
80
|
+
if greyscale:
|
|
81
|
+
numpy_im = np.array(im_cropped)
|
|
82
|
+
shape = numpy_im.shape
|
|
83
|
+
numpy_im.tofile(new_file_name)
|
|
84
|
+
else:
|
|
85
|
+
numpy_im = np.array(im_cropped)
|
|
86
|
+
shape = numpy_im.shape
|
|
87
|
+
im_cropped.save(new_file_name)
|
|
82
88
|
|
|
83
89
|
# remove the original, larger image
|
|
84
90
|
if delete_original:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import re
|
|
6
|
+
import satnogs_webscraper.constants as cnst
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def save_dataset(observation_list, save_name):
|
|
11
|
+
df = get_dataset(observation_list)
|
|
12
|
+
df.to_csv(save_name, index=False)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_dataset(observation_list):
|
|
16
|
+
observations = []
|
|
17
|
+
for observation in observation_list:
|
|
18
|
+
file_name = os.path.join(cnst.directories['observations'], f'{observation}.json')
|
|
19
|
+
with open(file_name, "r") as file_in:
|
|
20
|
+
observations.append(json.load(file_in))
|
|
21
|
+
|
|
22
|
+
df = pd.DataFrame(observations)
|
|
23
|
+
return df
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_datasets(observation_list, kaitai_interface=None):
|
|
27
|
+
common_key = 0
|
|
28
|
+
|
|
29
|
+
observations = []
|
|
30
|
+
demod_dictionary = dict()
|
|
31
|
+
demods = []
|
|
32
|
+
|
|
33
|
+
for observation in observation_list:
|
|
34
|
+
file_name = os.path.join(cnst.directories['observations'], f'{observation}.json')
|
|
35
|
+
with open(file_name, "r") as file_in:
|
|
36
|
+
observation_dictionary = json.load(file_in)
|
|
37
|
+
observation_dictionary['meta_key'] = common_key if len(observation_dictionary['demods']) > 0 else -1
|
|
38
|
+
|
|
39
|
+
for demod in observation_dictionary['demods']:
|
|
40
|
+
with open(demod['location'], 'rb') as file_in:
|
|
41
|
+
demod_data = file_in.read()
|
|
42
|
+
demods.append({
|
|
43
|
+
'timestamp': get_demod_time(demod['original_name']),
|
|
44
|
+
'bytes': demod_data,
|
|
45
|
+
'meta_key': common_key
|
|
46
|
+
})
|
|
47
|
+
if kaitai_interface is not None:
|
|
48
|
+
from kaitaistruct import KaitaiStream, BytesIO
|
|
49
|
+
if 'kaitai' not in observation_dictionary.keys():
|
|
50
|
+
observation_dictionary['kaitai'] = []
|
|
51
|
+
try:
|
|
52
|
+
observation_dictionary['kaitai'].append(kaitai_interface(KaitaiStream(BytesIO(demod_data))))
|
|
53
|
+
except Exception as e:
|
|
54
|
+
warnings.warn(f"Error parsing demod. Error:{e}")
|
|
55
|
+
pass
|
|
56
|
+
observations.append(observation_dictionary)
|
|
57
|
+
common_key += 1
|
|
58
|
+
|
|
59
|
+
meta_df = pd.DataFrame(observations)
|
|
60
|
+
meta_df.set_index(['Observation_id'], inplace=True)
|
|
61
|
+
meta_df.sort_index(inplace=True)
|
|
62
|
+
|
|
63
|
+
for demod in demods:
|
|
64
|
+
length_key = len(demod['bytes'])
|
|
65
|
+
|
|
66
|
+
if length_key not in demod_dictionary.keys():
|
|
67
|
+
demod_dictionary[length_key] = dict()
|
|
68
|
+
demod_dictionary[length_key]['demods'] = []
|
|
69
|
+
demod_dictionary[length_key]['demod_length'] = length_key
|
|
70
|
+
demod_dictionary[length_key]['num_demods'] = 0
|
|
71
|
+
|
|
72
|
+
for k, v in enumerate(demod['bytes']):
|
|
73
|
+
demod[k] = v
|
|
74
|
+
|
|
75
|
+
del demod['bytes']
|
|
76
|
+
|
|
77
|
+
demod_dictionary[length_key]['demods'].append(demod)
|
|
78
|
+
demod_dictionary[length_key]['num_demods'] += 1
|
|
79
|
+
|
|
80
|
+
for key in demod_dictionary.keys():
|
|
81
|
+
df = pd.DataFrame(demod_dictionary[key]['demods'])
|
|
82
|
+
df.set_index(['timestamp'], inplace=True)
|
|
83
|
+
df.sort_index(inplace=True)
|
|
84
|
+
demod_dictionary[key]['dataframe'] = df
|
|
85
|
+
del demod_dictionary[key]['demods']
|
|
86
|
+
|
|
87
|
+
if len(demod_dictionary.keys()) != 0:
|
|
88
|
+
demod_df = pd.DataFrame([demod_dictionary[key] for key in demod_dictionary.keys()])
|
|
89
|
+
demod_df.set_index(['demod_length'], inplace=True)
|
|
90
|
+
demod_df.sort_values(['num_demods'], ascending=False, inplace=True)
|
|
91
|
+
return meta_df, demod_df
|
|
92
|
+
else:
|
|
93
|
+
return meta_df, None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_demod_time(demod_url):
|
|
97
|
+
|
|
98
|
+
pattern = r"\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:_\d+)?"
|
|
99
|
+
match = re.search(pattern, demod_url)
|
|
100
|
+
time_string = match.group(0)
|
|
101
|
+
|
|
102
|
+
if time_string.find("_") == -1:
|
|
103
|
+
time_parsed = datetime.datetime.strptime(time_string, "%Y-%m-%dT%H-%M-%S")
|
|
104
|
+
else:
|
|
105
|
+
time_stamp_part = time_string.split("_")[0]
|
|
106
|
+
extra_part = int(time_string.split("_")[1])
|
|
107
|
+
time_parsed = datetime.datetime.strptime(time_stamp_part, "%Y-%m-%dT%H-%M-%S")
|
|
108
|
+
time_parsed = time_parsed + datetime.timedelta(milliseconds=extra_part)
|
|
109
|
+
return time_parsed
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def find_largest_number(string):
|
|
113
|
+
numbers = re.findall(r'\d+', string)
|
|
114
|
+
numbers = list(map(int, numbers))
|
|
115
|
+
return max(numbers) if numbers else 0
|
{satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_scraper.py
RENAMED
|
@@ -17,7 +17,7 @@ import satnogs_webscraper.progress_utils as pu
|
|
|
17
17
|
|
|
18
18
|
class ObservationScraper:
|
|
19
19
|
def __init__(self, fetch_waterfalls=True, fetch_logging=True, prints=True, check_disk=True, cpus=1,
|
|
20
|
-
grey_scale=True):
|
|
20
|
+
grey_scale=True, delete_original_waterfall=True):
|
|
21
21
|
"""
|
|
22
22
|
Scrapes the webpages for satellite observations. Waterfall fetches are set to false by default due to the
|
|
23
23
|
very large file sizes.
|
|
@@ -34,7 +34,7 @@ class ObservationScraper:
|
|
|
34
34
|
self.log_file_loc = cnst.files["log_file"]
|
|
35
35
|
self.waterfall_path = cnst.directories['waterfalls']
|
|
36
36
|
self.demod_path = cnst.directories['demods']
|
|
37
|
-
|
|
37
|
+
self.delete_original_waterfall = delete_original_waterfall
|
|
38
38
|
self.prints = prints
|
|
39
39
|
self.check_disk = check_disk
|
|
40
40
|
cnst.verify_directories()
|
|
@@ -49,7 +49,7 @@ class ObservationScraper:
|
|
|
49
49
|
"""
|
|
50
50
|
urls = [f'{cnst.web_address}{cnst.observations}{observation}/' for observation in observations_list]
|
|
51
51
|
|
|
52
|
-
#self.progress_dict = pu.setup_progress_dict(items_total=len(urls), items_done=0)
|
|
52
|
+
# self.progress_dict = pu.setup_progress_dict(items_total=len(urls), items_done=0)
|
|
53
53
|
|
|
54
54
|
# pool = Pool(self.cpus)
|
|
55
55
|
# self.observations_list = pool.map(self.scrape_observation, urls)
|
|
@@ -90,6 +90,10 @@ class ObservationScraper:
|
|
|
90
90
|
if waterfall_status is not None:
|
|
91
91
|
template['Waterfall_Status'] = waterfall_status.text.strip()
|
|
92
92
|
|
|
93
|
+
meta_data_element = observation_web_page.find('pre', id='json-metadata')
|
|
94
|
+
meta_data_json = meta_data_element['data-json']
|
|
95
|
+
template['Metadata'] = json.loads(meta_data_json)
|
|
96
|
+
|
|
93
97
|
status = observation_web_page.select("#rating-status > span")
|
|
94
98
|
if (status is not None) & (status[0] is not None):
|
|
95
99
|
template['Status'] = status[0].text.strip()
|
|
@@ -97,10 +101,12 @@ class ObservationScraper:
|
|
|
97
101
|
template['Observation_id'] = observation
|
|
98
102
|
|
|
99
103
|
template['demods'] = []
|
|
104
|
+
demod_tasks = []
|
|
100
105
|
|
|
101
106
|
for data_a in observation_web_page.find_all("a", class_='data-link'):
|
|
102
|
-
|
|
107
|
+
demod_tasks.append(dask.delayed(self.fetch_demod)(data_a))
|
|
103
108
|
|
|
109
|
+
template['demods'] = dask.compute(*[demod_tasks])[0]
|
|
104
110
|
with open(os.path.join(cnst.directories['observations'], f"{observation}.json"), 'w') as obs_out:
|
|
105
111
|
json.dump(template, obs_out)
|
|
106
112
|
|
|
@@ -133,7 +139,7 @@ class ObservationScraper:
|
|
|
133
139
|
except:
|
|
134
140
|
return "Satellite", ""
|
|
135
141
|
|
|
136
|
-
if contents.find("Station") != -1:
|
|
142
|
+
if (contents.find("Station") != -1) and (contents.find("Owner") == -1):
|
|
137
143
|
try:
|
|
138
144
|
second_element = tr.select_one('td:nth-child(2)')
|
|
139
145
|
second_element = second_element.find("a")
|
|
@@ -141,12 +147,14 @@ class ObservationScraper:
|
|
|
141
147
|
except:
|
|
142
148
|
return "Station", ""
|
|
143
149
|
|
|
144
|
-
if contents.find("
|
|
150
|
+
if contents.find("Timeframe") != -1:
|
|
145
151
|
try:
|
|
146
152
|
second_element = tr.select_one('td:nth-child(2)')
|
|
147
|
-
|
|
153
|
+
dates = second_element.find_all('span', class_='datetime-date')
|
|
154
|
+
times = second_element.find_all('span', class_='datetime-time')
|
|
155
|
+
return "Timeframe", [f"{date.text} {time.text}" for date, time in zip(dates, times)]
|
|
148
156
|
except:
|
|
149
|
-
return "
|
|
157
|
+
return "Timeframe", []
|
|
150
158
|
|
|
151
159
|
if contents.find("Frequency") != -1:
|
|
152
160
|
try:
|
|
@@ -159,18 +167,11 @@ class ObservationScraper:
|
|
|
159
167
|
if contents.find("Mode") != -1:
|
|
160
168
|
try:
|
|
161
169
|
second_element = tr.select_one('td:nth-child(2)')
|
|
162
|
-
return "Mode",
|
|
170
|
+
return "Mode", " ".join(
|
|
171
|
+
[span.text.strip() for span in second_element.select("span") if span is not None])
|
|
163
172
|
except:
|
|
164
173
|
return "Mode", ""
|
|
165
174
|
|
|
166
|
-
if contents.find("Metadata") != -1:
|
|
167
|
-
try:
|
|
168
|
-
second_element = tr.select_one('td:nth-child(2)')
|
|
169
|
-
element = second_element.find("pre")
|
|
170
|
-
return "Metadata", element.attrs['data-json']
|
|
171
|
-
except:
|
|
172
|
-
return "Metadata", ""
|
|
173
|
-
|
|
174
175
|
if contents.find("Polar Plot") != -1:
|
|
175
176
|
element = tr.select_one("svg")
|
|
176
177
|
try:
|
|
@@ -213,12 +214,13 @@ class ObservationScraper:
|
|
|
213
214
|
:return: The shape of the cropped image and name of the waterfall written to disk as a bytes object.
|
|
214
215
|
"""
|
|
215
216
|
res = ru.get_request(url)
|
|
216
|
-
waterfall_name = os.path.abspath(self.waterfall_path + file_name)
|
|
217
|
+
waterfall_name = os.path.abspath(self.waterfall_path + file_name) +"_original"
|
|
217
218
|
|
|
218
219
|
with open(waterfall_name, 'wb') as out:
|
|
219
220
|
out.write(res.content)
|
|
220
221
|
|
|
221
|
-
cropped_shape, bytes_name = iu.crop_and_save_psd(waterfall_name, greyscale=self.grey_scale
|
|
222
|
+
cropped_shape, bytes_name = iu.crop_and_save_psd(waterfall_name, greyscale=self.grey_scale,
|
|
223
|
+
delete_original=self.delete_original_waterfall)
|
|
222
224
|
|
|
223
225
|
return cropped_shape, bytes_name
|
|
224
226
|
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
import satnogs_webscraper.constants as cnst
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def get_request(url):
|
|
10
|
+
def get_request(url, max_count=None):
|
|
11
11
|
count = 1
|
|
12
12
|
while True:
|
|
13
13
|
try:
|
|
@@ -17,7 +17,7 @@ def get_request(url):
|
|
|
17
17
|
return res
|
|
18
18
|
else:
|
|
19
19
|
sleep_amount = count ** 2
|
|
20
|
-
write_log(url,res.status_code, f"Timeout Count: {count}")
|
|
20
|
+
write_log(url, res.status_code, f"Timeout Count: {count}")
|
|
21
21
|
time.sleep(sleep_amount)
|
|
22
22
|
except Exception as e:
|
|
23
23
|
sleep_amount = count ** 2
|
|
@@ -26,6 +26,9 @@ def get_request(url):
|
|
|
26
26
|
if count == 10:
|
|
27
27
|
return None
|
|
28
28
|
count += 1
|
|
29
|
+
if max_count is not None:
|
|
30
|
+
if count >= max_count:
|
|
31
|
+
return None
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def write_log(url, code, comment=""):
|
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import satnogs_webscraper.constants as cnst
|
|
7
7
|
import satnogs_webscraper.observation_scraper as obs
|
|
8
8
|
import satnogs_webscraper.observation_list_scraper as ols
|
|
9
|
-
from satnogs_webscraper.observation_dataset import get_dataset
|
|
9
|
+
from satnogs_webscraper.observation_dataset import get_dataset, get_datasets
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Results(Enum):
|
|
@@ -43,6 +43,8 @@ class Scraper:
|
|
|
43
43
|
save_name: str = "scrape"
|
|
44
44
|
cpus: int = 0
|
|
45
45
|
grey_scale: bool = True
|
|
46
|
+
kaitai_interface: object = None
|
|
47
|
+
meta_df_only: bool = False
|
|
46
48
|
|
|
47
49
|
def __post_init__(self):
|
|
48
50
|
self.waterfall = Results(self.waterfall)
|
|
@@ -133,7 +135,11 @@ class Scraper:
|
|
|
133
135
|
print("Scraping Observation Pages...")
|
|
134
136
|
obs_scraper = obs.ObservationScraper(cpus=self.cpus, grey_scale=self.grey_scale)
|
|
135
137
|
obs_scraper.multiprocess_scrape_observations(ids)
|
|
136
|
-
|
|
138
|
+
|
|
139
|
+
if self.meta_df_only:
|
|
140
|
+
return get_dataset(ids)
|
|
141
|
+
else:
|
|
142
|
+
return get_datasets(ids, self.kaitai_interface)
|
|
137
143
|
|
|
138
144
|
|
|
139
145
|
if __name__ == '__main__':
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
|
-
Name:
|
|
3
|
-
Version: 1.4.
|
|
2
|
+
Name: satnogs_webscraper
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: Scrape SATNOGS data into a pandas dataframe
|
|
5
|
-
Home-page: UNKNOWN
|
|
6
5
|
Author: Ada L
|
|
7
6
|
Author-email: the.nostra.tymus@gmail.com
|
|
8
7
|
License: MIT
|
|
9
8
|
Keywords: satellite satnogs
|
|
10
|
-
Platform: UNKNOWN
|
|
11
9
|
Classifier: Development Status :: 3 - Alpha
|
|
12
10
|
Classifier: Intended Audience :: Developers
|
|
13
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
12
|
Classifier: Programming Language :: Python :: 3
|
|
15
13
|
Description-Content-Type: text/markdown
|
|
16
14
|
License-File: LICENSE.rst
|
|
15
|
+
Requires-Dist: pillow
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: bs4
|
|
19
|
+
Requires-Dist: html5lib
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Requires-Dist: dask[complete]
|
|
22
|
+
Requires-Dist: pytest
|
|
17
23
|
|
|
18
24
|
A webscraper to pull multiple satnogs observations into a single pandas dataframe
|
|
19
|
-
|
{satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/SOURCES.txt
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
LICENSE.rst
|
|
2
2
|
README.md
|
|
3
|
-
README.rst
|
|
4
3
|
setup.cfg
|
|
5
4
|
setup.py
|
|
6
5
|
satnogs_webscraper/__init__.py
|
|
@@ -17,4 +16,11 @@ satnogs_webscraper.egg-info/PKG-INFO
|
|
|
17
16
|
satnogs_webscraper.egg-info/SOURCES.txt
|
|
18
17
|
satnogs_webscraper.egg-info/dependency_links.txt
|
|
19
18
|
satnogs_webscraper.egg-info/requires.txt
|
|
20
|
-
satnogs_webscraper.egg-info/top_level.txt
|
|
19
|
+
satnogs_webscraper.egg-info/top_level.txt
|
|
20
|
+
tests/test_constants.py
|
|
21
|
+
tests/test_image_utils.py
|
|
22
|
+
tests/test_observation_dataset.py
|
|
23
|
+
tests/test_observation_list_scraper.py
|
|
24
|
+
tests/test_observation_scraper.py
|
|
25
|
+
tests/test_request_utils.py
|
|
26
|
+
tests/test_scraper.py
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import satnogs_webscraper.constants as cnst
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_verify_directories():
|
|
7
|
+
if os.path.exists(cnst.directories['data']):
|
|
8
|
+
shutil.rmtree(cnst.directories['data'])
|
|
9
|
+
|
|
10
|
+
cnst.verify_directories()
|
|
11
|
+
|
|
12
|
+
for key in cnst.directories.keys():
|
|
13
|
+
assert os.path.exists(cnst.directories[key]), f"Verify {key}:{cnst.directories[key]} exists"
|
|
14
|
+
|
|
15
|
+
if os.path.exists(cnst.directories['data']):
|
|
16
|
+
shutil.rmtree(cnst.directories['data'])
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
import satnogs_webscraper.image_utils as iu
|
|
6
|
+
from satnogs_webscraper.observation_scraper import ObservationScraper
|
|
7
|
+
import satnogs_webscraper.constants as cnst
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture()
|
|
12
|
+
def get_image_greyscale():
|
|
13
|
+
cnst.verify_directories()
|
|
14
|
+
scraper = ObservationScraper(grey_scale=False, delete_original_waterfall=False)
|
|
15
|
+
scrape = scraper.scrape_observation('https://network.satnogs.org/observations/5025420/')
|
|
16
|
+
original_name = scrape['Downloads']['waterfall_hash_name']+"_original"
|
|
17
|
+
yield Image.open(original_name).convert('L')
|
|
18
|
+
shutil.rmtree(cnst.directories['data'])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture()
|
|
23
|
+
def get_image():
|
|
24
|
+
cnst.verify_directories()
|
|
25
|
+
scraper = ObservationScraper(grey_scale=False, delete_original_waterfall=False)
|
|
26
|
+
scrape = scraper.scrape_observation('https://network.satnogs.org/observations/5025420/')
|
|
27
|
+
original_name = scrape['Downloads']['waterfall_hash_name']+"_original"
|
|
28
|
+
yield original_name
|
|
29
|
+
shutil.rmtree(cnst.directories['data'])
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_find_left_bound(get_image_greyscale):
|
|
33
|
+
assert 65 == iu.find_left_bound(get_image_greyscale)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_find_bottom_bound(get_image_greyscale):
|
|
37
|
+
assert 1551 == iu.find_bottom_bound(get_image_greyscale)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_find_right_bound(get_image_greyscale):
|
|
41
|
+
assert 688 == iu.find_right_bound(get_image_greyscale)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_find_upper_bound(get_image_greyscale):
|
|
45
|
+
assert 9 == iu.find_upper_bound(get_image_greyscale)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_crop_and_save_delete(get_image):
|
|
49
|
+
assert os.path.exists(get_image), "Verify the base image exists before function call"
|
|
50
|
+
_, name = iu.crop_and_save_psd(get_image)
|
|
51
|
+
assert not os.path.exists(get_image), "Verify the base image is removed after function call"
|
|
52
|
+
assert os.path.exists(name), "Verify the numpy image exists"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_crop_and_save_resize(get_image):
|
|
56
|
+
default_size = (623, 1542)
|
|
57
|
+
size, _ = iu.crop_and_save_psd(get_image, delete_original=False)
|
|
58
|
+
assert size[0] == default_size[1], "Verify default resize"
|
|
59
|
+
assert size[1] == default_size[0], "Verify default resize"
|
|
60
|
+
|
|
61
|
+
resize_dimen = (5, 10)
|
|
62
|
+
size, _ = iu.crop_and_save_psd(get_image, delete_original=False, resize=True, resize_dimen=resize_dimen)
|
|
63
|
+
assert size[0] == resize_dimen[1], "Verify custom resize"
|
|
64
|
+
assert size[1] == resize_dimen[0], "Verify custom resize"
|
|
65
|
+
|
|
66
|
+
original_dimen = (623, 1542)
|
|
67
|
+
size, _ = iu.crop_and_save_psd(get_image, delete_original=False, resize=False)
|
|
68
|
+
assert size[0] == original_dimen[1], "Verify no resize"
|
|
69
|
+
assert size[1] == original_dimen[0], "Verify no resize"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
import satnogs_webscraper.observation_dataset as od
|
|
6
|
+
import satnogs_webscraper.constants as cnst
|
|
7
|
+
from satnogs_webscraper.observation_scraper import ObservationScraper
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture()
|
|
11
|
+
def get_obs_ids():
|
|
12
|
+
if os.path.exists(cnst.directories['data']):
|
|
13
|
+
shutil.rmtree(cnst.directories['data'])
|
|
14
|
+
obs_list = ["5738648", "5740805", "5740806"]
|
|
15
|
+
cnst.verify_directories()
|
|
16
|
+
scraper = ObservationScraper(check_disk=False)
|
|
17
|
+
scraper.multiprocess_scrape_observations(obs_list)
|
|
18
|
+
yield obs_list
|
|
19
|
+
|
|
20
|
+
shutil.rmtree(cnst.directories['data'])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_get_dataset(get_obs_ids):
|
|
24
|
+
df = od.get_dataset(get_obs_ids)
|
|
25
|
+
assert df['Observation_id'].iloc[0] == "5738648"
|
|
26
|
+
assert df['Observation_id'].iloc[1] == "5740805"
|
|
27
|
+
assert df['Observation_id'].iloc[2] == "5740806"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_save_dataset(get_obs_ids):
|
|
31
|
+
save_name = "saved_observations.csv"
|
|
32
|
+
|
|
33
|
+
if os.path.exists(save_name):
|
|
34
|
+
os.remove(save_name)
|
|
35
|
+
|
|
36
|
+
od.save_dataset(get_obs_ids, save_name)
|
|
37
|
+
|
|
38
|
+
assert os.path.exists(save_name)
|
|
39
|
+
|
|
40
|
+
df = pd.read_csv(save_name)
|
|
41
|
+
assert df['Observation_id'].iloc[0] == 5738648
|
|
42
|
+
assert df['Observation_id'].iloc[1] == 5740805
|
|
43
|
+
assert df['Observation_id'].iloc[2] == 5740806
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_get_datasets(get_obs_ids):
|
|
47
|
+
meta_df, demod_df = od.get_datasets(get_obs_ids)
|
|
48
|
+
|
|
49
|
+
assert demod_df is None
|
|
50
|
+
assert meta_df.shape[0] == 3
|
|
51
|
+
assert meta_df.shape[1] == 13
|
|
52
|
+
assert '5740805' in meta_df.index
|
|
53
|
+
assert '5738648' in meta_df.index
|
|
54
|
+
assert '5740806' in meta_df.index
|
|
55
|
+
|
|
56
|
+
for idx in range(0,3):
|
|
57
|
+
assert meta_df.iloc[idx].meta_key == -1
|
|
58
|
+
|
|
59
|
+
for expected_column in ['Timeframe', 'Satellite', 'Station', 'Status', 'Status_Message',
|
|
60
|
+
'Frequency', 'Mode', 'Metadata', 'Downloads', 'Waterfall_Status',
|
|
61
|
+
'Polar_Plot', 'meta_key']:
|
|
62
|
+
assert expected_column in meta_df.columns
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import satnogs_webscraper.observation_list_scraper as ols
|
|
2
|
+
import satnogs_webscraper.constants as cnst
|
|
3
|
+
import pytest
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import re
|
|
7
|
+
import json
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture()
|
|
12
|
+
def prep_directories():
|
|
13
|
+
if os.path.exists(cnst.directories['data']):
|
|
14
|
+
shutil.rmtree(cnst.directories['data'])
|
|
15
|
+
cnst.verify_directories()
|
|
16
|
+
yield None
|
|
17
|
+
shutil.rmtree(cnst.directories['data'])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_observation_list_scraper_init():
|
|
21
|
+
url = 'some_url'
|
|
22
|
+
save_name = "save_name"
|
|
23
|
+
save_dir = "some_dir"
|
|
24
|
+
page_limit = 2
|
|
25
|
+
resume = False
|
|
26
|
+
cpus = 2
|
|
27
|
+
fetcher1 = ols.ObservationListFetch(url, save_name, save_dir)
|
|
28
|
+
fetcher2 = ols.ObservationListFetch(url, save_name, save_dir, page_limit=page_limit, resume=resume, cpus=cpus)
|
|
29
|
+
|
|
30
|
+
assert url == fetcher1.url
|
|
31
|
+
assert save_name == fetcher1.save_name
|
|
32
|
+
assert save_dir == fetcher1.save_dir
|
|
33
|
+
assert fetcher1.page_limit is None
|
|
34
|
+
assert fetcher1.cpus is None
|
|
35
|
+
assert fetcher1.resume == True
|
|
36
|
+
|
|
37
|
+
assert url == fetcher2.url
|
|
38
|
+
assert save_name == fetcher2.save_name
|
|
39
|
+
assert save_dir == fetcher2.save_dir
|
|
40
|
+
assert page_limit == fetcher2.page_limit
|
|
41
|
+
assert resume == fetcher2.resume
|
|
42
|
+
assert cpus == fetcher2.cpus
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_get_pages(prep_directories):
|
|
46
|
+
test_url = "https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=&observer=&station" \
|
|
47
|
+
"=&start=&end=&transmitter_mode="
|
|
48
|
+
|
|
49
|
+
fetcher1 = ols.ObservationListFetch(test_url, "", "", page_limit=2)
|
|
50
|
+
|
|
51
|
+
pages = fetcher1.get_pages(test_url)
|
|
52
|
+
|
|
53
|
+
assert len(pages) == 2
|
|
54
|
+
|
|
55
|
+
for page in pages:
|
|
56
|
+
assert 200 == requests.get(page).status_code
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_get_page_observation_ids(prep_directories):
|
|
60
|
+
test_url = "https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=&observer=&station" \
|
|
61
|
+
"=&start=&end=&transmitter_mode="
|
|
62
|
+
|
|
63
|
+
fetcher1 = ols.ObservationListFetch(test_url, "", "", page_limit=2)
|
|
64
|
+
pages = fetcher1.get_pages(test_url)
|
|
65
|
+
|
|
66
|
+
ids_page0 = fetcher1.get_page_observation_ids(pages[0])
|
|
67
|
+
assert len(ids_page0) == 20
|
|
68
|
+
for id in ids_page0:
|
|
69
|
+
assert id == re.search(r"\d+", id)[0]
|
|
70
|
+
|
|
71
|
+
ids_page1 = fetcher1.get_page_observation_ids(pages[1])
|
|
72
|
+
assert len(ids_page1) == 20
|
|
73
|
+
for id in ids_page1:
|
|
74
|
+
assert id == re.search(r"\d+", id)[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_multiprocess_id_fetch(prep_directories):
|
|
78
|
+
test_url = "https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=&observer=&station" \
|
|
79
|
+
"=&start=&end=&transmitter_mode="
|
|
80
|
+
json1 = cnst.directories['observation_pages'] + "1.json"
|
|
81
|
+
json2 = cnst.directories['observation_pages'] + "2.json"
|
|
82
|
+
|
|
83
|
+
fetcher1 = ols.ObservationListFetch(test_url, "", page_limit=2)
|
|
84
|
+
fetcher1.multiprocess_id_fetch()
|
|
85
|
+
|
|
86
|
+
assert os.path.exists(json1)
|
|
87
|
+
assert os.path.exists(json2)
|
|
88
|
+
|
|
89
|
+
with open(json1) as fin:
|
|
90
|
+
json1_parsed = json.load(fin)
|
|
91
|
+
|
|
92
|
+
assert len(json1_parsed['IDs']) == 20
|
|
93
|
+
for id in json1_parsed['IDs']:
|
|
94
|
+
assert id == re.search(r"\d+", id)[0]
|
|
95
|
+
|
|
96
|
+
with open(json2) as fin:
|
|
97
|
+
json2_parsed = json.load(fin)
|
|
98
|
+
|
|
99
|
+
assert len(json2_parsed['IDs']) == 20
|
|
100
|
+
for id in json2_parsed['IDs']:
|
|
101
|
+
assert id == re.search(r"\d+", id)[0]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_fetch_ids(prep_directories):
|
|
105
|
+
if os.path.exists("test.json"):
|
|
106
|
+
os.remove("test.json")
|
|
107
|
+
|
|
108
|
+
test_url = "https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=&observer=&station" \
|
|
109
|
+
"=&start=&end=&transmitter_mode="
|
|
110
|
+
|
|
111
|
+
fetcher1 = ols.ObservationListFetch(test_url, "test.json", page_limit=2)
|
|
112
|
+
|
|
113
|
+
assert not os.path.exists("test.json")
|
|
114
|
+
|
|
115
|
+
ids = fetcher1.fetch_ids()
|
|
116
|
+
|
|
117
|
+
assert os.path.exists("test.json")
|
|
118
|
+
|
|
119
|
+
assert len(ids) == 40
|
|
120
|
+
|
|
121
|
+
for id in ids:
|
|
122
|
+
assert id == re.search(r"\d+", id)[0]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os.path
|
|
3
|
+
import shutil
|
|
4
|
+
import re
|
|
5
|
+
import satnogs_webscraper.constants as cnst
|
|
6
|
+
from satnogs_webscraper.observation_scraper import ObservationScraper
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
record_7206380 = {
|
|
10
|
+
'Observation_id': '7206380',
|
|
11
|
+
'Timeframe': ['2023-02-25 21:08:54', '2023-02-25 21:13:09'],
|
|
12
|
+
'Satellite': '43880 - UWE-4',
|
|
13
|
+
'Station': '2802 - geoscan-2',
|
|
14
|
+
'Status': 'Good',
|
|
15
|
+
'Status_Message': '100',
|
|
16
|
+
'Frequency': '435,599,000Hz',
|
|
17
|
+
'Mode': 'FSK 9600',
|
|
18
|
+
'Metadata': {'radio': {'name': 'gr-satnogs',
|
|
19
|
+
'version': 'v2.3-compat-xxx-v2.3.3.0',
|
|
20
|
+
'parameters': {'soapy-rx-device': 'driver=airspy,biastee=false,bitpack=true',
|
|
21
|
+
'samp-rate-rx': '6e6',
|
|
22
|
+
'rx-freq': '435599000',
|
|
23
|
+
'file-path': '/tmp/.satnogs/data/receiving_satnogs_7206380_2023-02-25T21-08-54.out',
|
|
24
|
+
'waterfall-file-path': '/tmp/.satnogs/data/receiving_waterfall_7206380_2023-02-25T21-08-54.dat',
|
|
25
|
+
'decoded-data-file-path': '/tmp/.satnogs/data/data_7206380',
|
|
26
|
+
'doppler-correction-per-sec': None,
|
|
27
|
+
'lo-offset': None,
|
|
28
|
+
'ppm': None,
|
|
29
|
+
'rigctl-port': '4532',
|
|
30
|
+
'gain-mode': 'Settings Field',
|
|
31
|
+
'gain': None,
|
|
32
|
+
'antenna': 'RX',
|
|
33
|
+
'dev-args': None,
|
|
34
|
+
'stream-args': None,
|
|
35
|
+
'tune-args': None,
|
|
36
|
+
'other-settings': 'LNA=3,MIX=11,VGA=14',
|
|
37
|
+
'dc-removal': None,
|
|
38
|
+
'bb-freq': None,
|
|
39
|
+
'bw': None,
|
|
40
|
+
'enable-iq-dump': '1',
|
|
41
|
+
'iq-file-path': '/tmp/.satnogs/iq.bin',
|
|
42
|
+
'udp-dump-host': '127.0.0.1',
|
|
43
|
+
'udp-dump-port': 57356,
|
|
44
|
+
'wpm': None,
|
|
45
|
+
'baudrate': '9600',
|
|
46
|
+
'framing': 'ax25'}},
|
|
47
|
+
'latitude': 60.002677,
|
|
48
|
+
'longitude': 30.364098,
|
|
49
|
+
'elevation': 50,
|
|
50
|
+
'frequency': 435599000},
|
|
51
|
+
'Downloads': {
|
|
52
|
+
'audio': 'https://archive.org/download/satnogs-observations-007200001-007210000/satnogs-observations-007206001-007207000.zip/satnogs_7206380_2023-02-25T21-08-54.ogg',
|
|
53
|
+
'waterfall': 'https://s3.eu-central-1.wasabisys.com/satnogs-network/data_obs/2023/2/25/21/7206380/waterfall_7206380_2023-02-25T21-08-54.png',
|
|
54
|
+
'waterfall_hash_name': 'd4c1e87080299224b407920fc2ab56249b7961f28f4e57cfc565758f36250a6b',
|
|
55
|
+
'waterfall_shape': (1542, 623, 3)},
|
|
56
|
+
'Waterfall_Status': 'Unknown',
|
|
57
|
+
'Polar_Plot': {'tle1': '1 43880U 18111E 23056.19343271 .00005878 00000+0 47147-3 0 9994',
|
|
58
|
+
'tle2': '2 43880 97.6498 323.1620 0014413 186.0212 174.0841 15.00395149227562',
|
|
59
|
+
'timeframe-start': '2023-02-25T21:08:54+00:00',
|
|
60
|
+
'timeframe-end': '2023-02-25T21:13:09+00:00',
|
|
61
|
+
'groundstation-lat': '60.002677',
|
|
62
|
+
'groundstation-lon': '30.364098',
|
|
63
|
+
'groundstation-alt': '50'},
|
|
64
|
+
'demods': [{'original_name': 'data_obs/2023/2/25/21/7206380/data_7206380_2023-02-25T21-10-20_g0',
|
|
65
|
+
'location': '74851f09608f5c7f10a1d71460484c399b599a70352a58b1a990e3a7df3d13f0.bin'},
|
|
66
|
+
{'original_name': 'data_obs/2023/2/25/21/7206380/data_7206380_2023-02-25T21-10-28',
|
|
67
|
+
'location': 'c1126bc65bcfe09bbc97e574a7323c6b5f28db978e35b48c93a8b5f1e26505bd.bin'},
|
|
68
|
+
{'original_name': 'data_obs/2023/2/25/21/7206380/data_7206380_2023-02-25T21-10-47_g0',
|
|
69
|
+
'location': '0501c350339d0689505f8bcdf16236b2e1a7ecc6df90b706505c0f5f591d9773.bin'}]}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture()
|
|
73
|
+
def prep_directories():
|
|
74
|
+
if os.path.exists(cnst.directories['data']):
|
|
75
|
+
shutil.rmtree(cnst.directories['data'])
|
|
76
|
+
cnst.verify_directories()
|
|
77
|
+
yield None
|
|
78
|
+
shutil.rmtree(cnst.directories['data'])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_Observation_Scraper_init():
|
|
82
|
+
assert not os.path.exists(cnst.directories['data'])
|
|
83
|
+
|
|
84
|
+
fetch_waterfalls = True
|
|
85
|
+
fetch_logging = True
|
|
86
|
+
prints = True
|
|
87
|
+
check_disk = True
|
|
88
|
+
cpus = 1
|
|
89
|
+
grey_scale = True
|
|
90
|
+
|
|
91
|
+
obs_scraper = ObservationScraper()
|
|
92
|
+
|
|
93
|
+
assert os.path.exists(cnst.directories['data'])
|
|
94
|
+
assert fetch_waterfalls == obs_scraper.fetch_waterfalls
|
|
95
|
+
assert fetch_logging == obs_scraper.fetch_logging
|
|
96
|
+
assert prints == obs_scraper.prints
|
|
97
|
+
assert check_disk == obs_scraper.check_disk
|
|
98
|
+
assert cpus == obs_scraper.cpus
|
|
99
|
+
assert grey_scale == obs_scraper.grey_scale
|
|
100
|
+
assert isinstance(obs_scraper.observations_list, list)
|
|
101
|
+
assert cnst.files["observation_json"] == obs_scraper.json_file_loc
|
|
102
|
+
assert cnst.directories['observations'] == obs_scraper.observation_save_dir
|
|
103
|
+
assert cnst.files["log_file"] == obs_scraper.log_file_loc
|
|
104
|
+
assert cnst.directories['waterfalls'] == obs_scraper.waterfall_path
|
|
105
|
+
assert cnst.directories["demods"] == obs_scraper.demod_path
|
|
106
|
+
|
|
107
|
+
fetch_waterfalls = False
|
|
108
|
+
fetch_logging = False
|
|
109
|
+
prints = False
|
|
110
|
+
check_disk = False
|
|
111
|
+
cpus = 2
|
|
112
|
+
grey_scale = False
|
|
113
|
+
|
|
114
|
+
obs_scraper2 = ObservationScraper(fetch_waterfalls=fetch_waterfalls, fetch_logging=fetch_logging, prints=prints,
|
|
115
|
+
check_disk=check_disk, cpus=cpus, grey_scale=grey_scale)
|
|
116
|
+
|
|
117
|
+
assert fetch_waterfalls == obs_scraper2.fetch_waterfalls
|
|
118
|
+
assert fetch_logging == obs_scraper2.fetch_logging
|
|
119
|
+
assert prints == obs_scraper2.prints
|
|
120
|
+
assert check_disk == obs_scraper2.check_disk
|
|
121
|
+
assert cpus == obs_scraper2.cpus
|
|
122
|
+
assert grey_scale == obs_scraper2.grey_scale
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_observation_scrape(prep_directories):
|
|
126
|
+
observation_url = "https://network.satnogs.org/observations/7206380/"
|
|
127
|
+
obs_scraper = ObservationScraper()
|
|
128
|
+
|
|
129
|
+
scrape = obs_scraper.scrape_observation(url=observation_url)
|
|
130
|
+
|
|
131
|
+
keys_to_skip = ['demods', 'Metadata', 'Polar_Plot', 'Downloads', 'Timeframe']
|
|
132
|
+
|
|
133
|
+
# Test records that are in the table
|
|
134
|
+
for key in scrape.keys():
|
|
135
|
+
if key not in keys_to_skip:
|
|
136
|
+
assert scrape[key] == record_7206380[key]
|
|
137
|
+
|
|
138
|
+
# Test the timeframe record
|
|
139
|
+
assert 2 == len(scrape['Timeframe'])
|
|
140
|
+
assert scrape['Timeframe'][0] == record_7206380['Timeframe'][0]
|
|
141
|
+
assert scrape['Timeframe'][1] == record_7206380['Timeframe'][1]
|
|
142
|
+
|
|
143
|
+
# Test metadata
|
|
144
|
+
assert scrape['Metadata'] is not None
|
|
145
|
+
for key in record_7206380['Metadata'].keys():
|
|
146
|
+
assert record_7206380['Metadata'][key] == scrape['Metadata'][key]
|
|
147
|
+
|
|
148
|
+
# Test polar plot
|
|
149
|
+
assert scrape['Polar_Plot'] is not None
|
|
150
|
+
for key in record_7206380['Polar_Plot'].keys():
|
|
151
|
+
assert record_7206380['Polar_Plot'][key] == scrape['Polar_Plot'][key]
|
|
152
|
+
|
|
153
|
+
# Test the downloads
|
|
154
|
+
assert record_7206380['Downloads']['audio'] == scrape['Downloads']['audio']
|
|
155
|
+
assert record_7206380['Downloads']['waterfall'] == scrape['Downloads']['waterfall']
|
|
156
|
+
assert len(scrape['Downloads']['waterfall_shape']) == 2
|
|
157
|
+
assert scrape['Downloads']['waterfall_hash_name'].find(record_7206380['Downloads']['waterfall_hash_name']) != -1
|
|
158
|
+
|
|
159
|
+
# Test the demods
|
|
160
|
+
assert len(scrape['demods']) == len(record_7206380['demods'])
|
|
161
|
+
for s_demod, r_demod in zip(scrape['demods'], record_7206380['demods']):
|
|
162
|
+
assert s_demod['original_name'] == r_demod['original_name']
|
|
163
|
+
assert s_demod['location'].find(r_demod['location']) != -1
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_multi_process_observation_scrape(prep_directories):
|
|
167
|
+
obs_ids = [7206380, 7206656]
|
|
168
|
+
obs_names = [f'{obs_id}.json' for obs_id in obs_ids]
|
|
169
|
+
obs_scraper = ObservationScraper()
|
|
170
|
+
obs_scraper.multiprocess_scrape_observations(obs_ids)
|
|
171
|
+
|
|
172
|
+
scraped_files = os.listdir(cnst.directories['observations'])
|
|
173
|
+
for obs_name in obs_names:
|
|
174
|
+
assert obs_name in scraped_files
|
|
175
|
+
|
|
176
|
+
waterfall_files = [
|
|
177
|
+
"042cae91b4da57f450dda7a3504503c4d293ea9cbb268b087d51f484bae465e1.bin",
|
|
178
|
+
"d4c1e87080299224b407920fc2ab56249b7961f28f4e57cfc565758f36250a6b.bin"
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
processed_waterfall_files = os.listdir(cnst.directories['waterfalls'])
|
|
182
|
+
|
|
183
|
+
for file in waterfall_files:
|
|
184
|
+
assert file in processed_waterfall_files
|
|
185
|
+
|
|
186
|
+
demod_files = [
|
|
187
|
+
"0501c350339d0689505f8bcdf16236b2e1a7ecc6df90b706505c0f5f591d9773.bin",
|
|
188
|
+
"74851f09608f5c7f10a1d71460484c399b599a70352a58b1a990e3a7df3d13f0.bin",
|
|
189
|
+
"830ecb5c37750234323882591ff8a0f3e5c3030a95a41e56c72c90eb64f5acec.bin",
|
|
190
|
+
"99f4e4cc9364e7f704219ed05289b273a2df8136438f78ebb7aaf6a4349dd109.bin",
|
|
191
|
+
"c1126bc65bcfe09bbc97e574a7323c6b5f28db978e35b48c93a8b5f1e26505bd.bin"
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
processed_demod_files = os.listdir(cnst.directories['demods'])
|
|
195
|
+
|
|
196
|
+
for file in demod_files:
|
|
197
|
+
assert file in processed_demod_files
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os.path
|
|
3
|
+
import shutil
|
|
4
|
+
import satnogs_webscraper.constants as cnst
|
|
5
|
+
import satnogs_webscraper.request_utils as ru
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture()
|
|
10
|
+
def prep_directories():
|
|
11
|
+
|
|
12
|
+
if os.path.exists(cnst.directories['data']):
|
|
13
|
+
shutil.rmtree(cnst.directories['data'])
|
|
14
|
+
cnst.verify_directories()
|
|
15
|
+
yield None
|
|
16
|
+
shutil.rmtree(cnst.directories['data'])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_write_log(prep_directories):
|
|
20
|
+
assert len(os.listdir(cnst.directories['logs'])) == 0
|
|
21
|
+
|
|
22
|
+
url = "test/url"
|
|
23
|
+
code = 123
|
|
24
|
+
comment = "This is a comment"
|
|
25
|
+
log_file = ru.write_log(url=url, code=code, comment=comment)
|
|
26
|
+
|
|
27
|
+
assert len(os.listdir(cnst.directories['logs'])) == 1
|
|
28
|
+
|
|
29
|
+
with open(log_file) as file_in:
|
|
30
|
+
parsed_logs = json.load(file_in)
|
|
31
|
+
|
|
32
|
+
assert parsed_logs['url'] == url
|
|
33
|
+
assert parsed_logs['status'] == code
|
|
34
|
+
assert parsed_logs['comment'] == comment
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_get_request_logging(prep_directories):
|
|
38
|
+
url = 'https://www.google.com/'
|
|
39
|
+
|
|
40
|
+
assert len(os.listdir(cnst.directories['logs'])) == 0
|
|
41
|
+
|
|
42
|
+
res = ru.get_request(url)
|
|
43
|
+
|
|
44
|
+
assert len(os.listdir(cnst.directories['logs'])) == 1
|
|
45
|
+
|
|
46
|
+
log_file = os.listdir(cnst.directories['logs'])[0]
|
|
47
|
+
log_file = os.path.join(cnst.directories['logs'], log_file)
|
|
48
|
+
|
|
49
|
+
with open(log_file) as file_in:
|
|
50
|
+
parsed_logs = json.load(file_in)
|
|
51
|
+
|
|
52
|
+
assert parsed_logs['url'] == url
|
|
53
|
+
assert parsed_logs['status'] == res.status_code
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_get_request_exception_handling(prep_directories):
|
|
57
|
+
url = 'https://127.0.0.1/'
|
|
58
|
+
|
|
59
|
+
assert len(os.listdir(cnst.directories['logs'])) == 0
|
|
60
|
+
|
|
61
|
+
res = ru.get_request(url, max_count=2)
|
|
62
|
+
|
|
63
|
+
assert len(os.listdir(cnst.directories['logs'])) == 1
|
|
64
|
+
|
|
65
|
+
log_file = os.listdir(cnst.directories['logs'])[0]
|
|
66
|
+
log_file = os.path.join(cnst.directories['logs'], log_file)
|
|
67
|
+
|
|
68
|
+
with open(log_file) as file_in:
|
|
69
|
+
parsed_logs = json.load(file_in)
|
|
70
|
+
|
|
71
|
+
assert parsed_logs['url'] == url
|
|
72
|
+
assert parsed_logs['status'] == -1
|
|
73
|
+
assert parsed_logs['comment'].find("Exception") != -1
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import kaitaistruct
|
|
2
|
+
import importlib
|
|
3
|
+
import os.path
|
|
4
|
+
import shutil
|
|
5
|
+
import satnogs_webscraper.constants as cnst
|
|
6
|
+
from satnogs_webscraper.scraper import Scraper
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pytest
|
|
9
|
+
import subprocess
|
|
10
|
+
|
|
11
|
+
@pytest.fixture()
|
|
12
|
+
def prep_directories():
|
|
13
|
+
if os.path.exists(cnst.directories['data']):
|
|
14
|
+
shutil.rmtree(cnst.directories['data'])
|
|
15
|
+
cnst.verify_directories()
|
|
16
|
+
yield None
|
|
17
|
+
shutil.rmtree(cnst.directories['data'])
|
|
18
|
+
|
|
19
|
+
@pytest.fixture()
|
|
20
|
+
def get_kaitai_parser():
|
|
21
|
+
|
|
22
|
+
if os.path.exists(cnst.directories['data']):
|
|
23
|
+
shutil.rmtree(cnst.directories['data'])
|
|
24
|
+
cnst.verify_directories()
|
|
25
|
+
|
|
26
|
+
get_ksy = ["wget",
|
|
27
|
+
"https://gitlab.com/librespacefoundation/satnogs/satnogs-decoders/-/raw/master/ksy/armadillo.ksy"]
|
|
28
|
+
compile_ksy = ["kaitai-struct-compiler", "-t", "python", "armadillo.ksy"]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(get_ksy, check=True, capture_output=True, text=True)
|
|
32
|
+
print("Wget output:")
|
|
33
|
+
print(result.stdout)
|
|
34
|
+
except subprocess.CalledProcessError as e:
|
|
35
|
+
print(f"Wget failed with error: {e.stderr}")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(compile_ksy, check=True, capture_output=True, text=True)
|
|
39
|
+
print("Compile output:")
|
|
40
|
+
print(result.stdout)
|
|
41
|
+
except subprocess.CalledProcessError as e:
|
|
42
|
+
print(f"Command failed with error: {e.stderr}")
|
|
43
|
+
|
|
44
|
+
yield importlib.import_module('armadillo').Armadillo
|
|
45
|
+
|
|
46
|
+
shutil.rmtree(cnst.directories['data'])
|
|
47
|
+
os.remove("armadillo.py")
|
|
48
|
+
os.remove("armadillo.ksy")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_scraper_init(prep_directories):
|
|
52
|
+
expected_string = 'https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=&observer=&' \
|
|
53
|
+
'station=&start=&end=&transmitter_mode='
|
|
54
|
+
scraper = Scraper()
|
|
55
|
+
assert scraper.generate_query_string() == expected_string
|
|
56
|
+
|
|
57
|
+
expected_string2 = 'https://network.satnogs.org/observations/?future=0&bad=0&unknown=0&failed=0&norad=44352&' \
|
|
58
|
+
'observer=&station=&start=&end=&transmitter_mode='
|
|
59
|
+
|
|
60
|
+
scraper2 = Scraper(norad="44352")
|
|
61
|
+
assert scraper2.generate_query_string() == expected_string2
|
|
62
|
+
scraper3 = Scraper(norad="44352", kaitai_interface="Test")
|
|
63
|
+
assert scraper3.kaitai_interface == "Test"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_scraper_scrape_contents(prep_directories):
|
|
67
|
+
page_limit = 1
|
|
68
|
+
scraper = Scraper(norad="44352", list_page_limit=page_limit)
|
|
69
|
+
meta_df, demod_df = scraper.scrape()
|
|
70
|
+
|
|
71
|
+
# Testing the demod df container
|
|
72
|
+
assert demod_df.shape[0] == 6
|
|
73
|
+
assert demod_df.shape[1] == 2
|
|
74
|
+
assert 'num_demods' in demod_df.columns
|
|
75
|
+
assert 'dataframe' in demod_df.columns
|
|
76
|
+
assert 272 in demod_df.index
|
|
77
|
+
# Testing the best demod df scraped
|
|
78
|
+
demod_272 = demod_df.loc[272].dataframe
|
|
79
|
+
assert demod_272.shape[0] == 20
|
|
80
|
+
assert demod_272.shape[1] == 273
|
|
81
|
+
assert 'meta_key' in demod_272.columns
|
|
82
|
+
for i in range(0, 272):
|
|
83
|
+
assert i in demod_272.columns
|
|
84
|
+
|
|
85
|
+
# Testing meta df
|
|
86
|
+
assert meta_df.shape[0] == 20
|
|
87
|
+
assert meta_df.shape[1] == 13
|
|
88
|
+
for expected_column in ['Timeframe', 'Satellite', 'Station', 'Status', 'Status_Message',
|
|
89
|
+
'Frequency', 'Mode', 'Metadata', 'Downloads', 'Waterfall_Status',
|
|
90
|
+
'Polar_Plot', 'meta_key']:
|
|
91
|
+
assert expected_column in meta_df.columns
|
|
92
|
+
|
|
93
|
+
# Testing right join
|
|
94
|
+
|
|
95
|
+
df_merged = pd.merge(meta_df, demod_272, on='meta_key', how='right')
|
|
96
|
+
assert df_merged.shape[0] == 20
|
|
97
|
+
assert df_merged.shape[1] == 285
|
|
98
|
+
|
|
99
|
+
for expected_column in ['Timeframe', 'Satellite', 'Station', 'Status', 'Status_Message',
|
|
100
|
+
'Frequency', 'Mode', 'Metadata', 'Downloads', 'Waterfall_Status',
|
|
101
|
+
'Polar_Plot', 'meta_key']:
|
|
102
|
+
assert expected_column in df_merged.columns
|
|
103
|
+
|
|
104
|
+
for i in range(0, 272):
|
|
105
|
+
assert i in df_merged.columns
|
|
106
|
+
|
|
107
|
+
for name in meta_df['Satellite']:
|
|
108
|
+
assert name.find("ARMADILLO") != -1
|
|
109
|
+
assert name.find("44352") != -1
|
|
110
|
+
|
|
111
|
+
assert len(meta_df['Satellite'].unique()) == 1
|
|
112
|
+
assert sum(meta_df['Status'] == 'Good') == page_limit * 20
|
|
113
|
+
assert sum(meta_df['Status'] == 'Bad') == 0
|
|
114
|
+
|
|
115
|
+
scraper2 = Scraper(good=False, bad=True, list_page_limit=2)
|
|
116
|
+
meta_df, demod_df = scraper2.scrape()
|
|
117
|
+
assert len(meta_df['Satellite'].unique()) != 1
|
|
118
|
+
assert sum(meta_df['Status'] == 'Bad') == 2 * 20
|
|
119
|
+
assert sum(meta_df['Status'] == 'Good') == 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_kaitai_struct_interface(get_kaitai_parser):
|
|
123
|
+
scraper = Scraper(norad="44352", data=1, save_name="Armadillo", list_page_limit=3,
|
|
124
|
+
kaitai_interface=get_kaitai_parser)
|
|
125
|
+
|
|
126
|
+
meta_df, demod_df = scraper.scrape()
|
|
127
|
+
|
|
128
|
+
assert meta_df.shape[1] == 14
|
|
129
|
+
assert sum(meta_df['kaitai'].apply(len) > 0) > 10
|
|
130
|
+
|
|
131
|
+
kaitai_obj = meta_df[meta_df['kaitai'].apply(len) > 0].kaitai[0][0]
|
|
132
|
+
|
|
133
|
+
assert type(kaitai_obj._io) == kaitaistruct.KaitaiStream
|
|
134
|
+
|
|
135
|
+
assert kaitai_obj.ax25_frame is not None
|
|
136
|
+
|
|
File without changes
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import pandas as pd
|
|
4
|
-
import satnogs_webscraper.constants as cnst
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def save_dataset(observation_list, save_name):
|
|
8
|
-
df = get_dataset(observation_list)
|
|
9
|
-
df.to_csv(save_name, index=False)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def get_dataset(observation_list):
|
|
13
|
-
observations = []
|
|
14
|
-
for observation in observation_list:
|
|
15
|
-
file_name = os.path.join(cnst.directories['observations'], f'{observation}.json')
|
|
16
|
-
with open(file_name, "r") as file_in:
|
|
17
|
-
observations.append(json.load(file_in))
|
|
18
|
-
|
|
19
|
-
df = pd.DataFrame(observations)
|
|
20
|
-
return df
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_list_scraper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/top_level.txt
RENAMED
|
File without changes
|