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.
Files changed (29) hide show
  1. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/PKG-INFO +9 -4
  2. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/constants.py +2 -2
  3. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/image_utils.py +11 -5
  4. satnogs_webscraper-1.4.4/satnogs_webscraper/observation_dataset.py +115 -0
  5. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_scraper.py +21 -19
  6. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/request_utils.py +5 -2
  7. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/scraper.py +8 -2
  8. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/PKG-INFO +10 -5
  9. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/SOURCES.txt +8 -2
  10. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/setup.cfg +1 -1
  11. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/setup.py +1 -1
  12. satnogs_webscraper-1.4.4/tests/test_constants.py +16 -0
  13. satnogs_webscraper-1.4.4/tests/test_image_utils.py +69 -0
  14. satnogs_webscraper-1.4.4/tests/test_observation_dataset.py +64 -0
  15. satnogs_webscraper-1.4.4/tests/test_observation_list_scraper.py +122 -0
  16. satnogs_webscraper-1.4.4/tests/test_observation_scraper.py +197 -0
  17. satnogs_webscraper-1.4.4/tests/test_request_utils.py +73 -0
  18. satnogs_webscraper-1.4.4/tests/test_scraper.py +136 -0
  19. satnogs_webscraper-1.4.2/README.rst +0 -0
  20. satnogs_webscraper-1.4.2/satnogs_webscraper/observation_dataset.py +0 -20
  21. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/LICENSE.rst +0 -0
  22. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/README.md +0 -0
  23. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/__init__.py +0 -0
  24. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/mainCLI.py +0 -0
  25. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/observation_list_scraper.py +0 -0
  26. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper/progress_utils.py +0 -0
  27. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/dependency_links.txt +0 -0
  28. {satnogs_webscraper-1.4.2 → satnogs_webscraper-1.4.4}/satnogs_webscraper.egg-info/requires.txt +4 -4
  29. {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.2
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
- # store the shape and write to a file
79
- shape = numpy_im.shape
80
- new_file_name = input_image[:-4]
81
- numpy_im.tofile(new_file_name)
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
@@ -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
- template['demods'].append(self.fetch_demod(data_a))
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("Transmitter") != -1:
150
+ if contents.find("Timeframe") != -1:
145
151
  try:
146
152
  second_element = tr.select_one('td:nth-child(2)')
147
- return "Transmitter", second_element.text.strip()
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 "Transmitter", ""
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", [span.text.strip() for span in second_element.select("span") if span is not None]
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
- return get_dataset(ids)
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: satnogs-webscraper
3
- Version: 1.4.2
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
-
@@ -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
@@ -1,5 +1,5 @@
1
1
  [metadata]
2
- description-file = README.md
2
+ description_file = README.md
3
3
  license_files = LICENSE.rst
4
4
 
5
5
  [egg_info]
@@ -1,6 +1,6 @@
1
1
  from setuptools import setup
2
2
 
3
- VERSION = '1.4.2'
3
+ VERSION = '1.4.4'
4
4
  DESCRIPTION = 'Scrape SATNOGS data into a pandas dataframe'
5
5
  LONG_DESCRIPTION = 'A webscraper to pull multiple satnogs observations into a single pandas dataframe'
6
6
 
@@ -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
@@ -1,8 +1,8 @@
1
+ pillow
2
+ numpy
3
+ requests
1
4
  bs4
2
- dask[complete]
3
5
  html5lib
4
- numpy
5
6
  pandas
6
- pillow
7
+ dask[complete]
7
8
  pytest
8
- requests