class TimeSeriesMetaData: """ Used to store metadata for `~sunpy.timeseries.TimeSeries` that enables multiple `sunpy.timeseries.TimeSeries` metadata to be concatenated in an organized fashion. Possible signatures:: TimeSeriesMetaData(meta=dict, timerange=TimeRange, colnames=list) TimeSeriesMetaData(meta=tuple) TimeSeriesMetaData(meta=list(tuples)) TimeSeriesMetaData(timerange=TimeRange) TimeSeriesMetaData(timerange=TimeRange, colnames=list) Parameters ---------- meta : `dict`, `MetaDict`, `tuple`, `list` The metadata giving details about the time series data/instrument. Defaults to `None`. timerange : `~sunpy.time.TimeRange` A `~sunpy.time.TimeRange` representing the timespan of the data. Defaults to `None`. colnames : `list` A mapping from column names in ``data`` to the physical units of that column. Defaults to `None`. Attributes ---------- metadata : `list` of `tuple` The list of 3-tuples which each represent a source files metadata. The tuples consist of: ``(TimeRange, [colnames], MetaDict(metadata))``. Examples -------- >>> from sunpy.timeseries import TimeSeriesMetaData >>> from sunpy.time import TimeRange, parse_time >>> from sunpy.util import MetaDict >>> tr = TimeRange('2012-06-01 00:00','2012-06-02 00:00') >>> md = TimeSeriesMetaData(timerange=tr, colnames=['GOES'], ... meta=MetaDict([('goes_key','goes_val')])) >>> tr2 = TimeRange('2012-06-01 12:00','2012-06-02 12:00') >>> md.append(tr2, ['EVE'], MetaDict([('eve_key','eve_val')])) >>> md.find(parse_time('2012-06-01T21:08:12')) |-------------------------------------------------------------------------------------------------| |TimeRange | Columns | Meta | |-------------------------------------------------------------------------------------------------| |2012-06-01T00:00:00.000 | GOES | goes_key: goes_val | | to | | | |2012-06-02T00:00:00.000 | | | |-------------------------------------------------------------------------------------------------| |2012-06-01T12:00:00.000 | EVE | eve_key: eve_val | | to | | | |2012-06-02T12:00:00.000 | | | |-------------------------------------------------------------------------------------------------| <BLANKLINE> >>> md.find(parse_time('2012-06-01T21:08:12')).columns ['EVE', 'GOES'] >>> md.find(parse_time('2012-06-01T21:08:12')).values() ['eve_val', 'goes_val'] >>> md.find(parse_time('2012-06-01T21:08:12')).metas [MetaDict([('goes_key', 'goes_val')]), MetaDict([('eve_key', 'eve_val')])] >>> md.find(parse_time('2012-06-01T21:08:12'), 'GOES') |-------------------------------------------------------------------------------------------------| |TimeRange | Columns | Meta | |-------------------------------------------------------------------------------------------------| |2012-06-01T00:00:00.000 | GOES | goes_key: goes_val | | to | | | |2012-06-02T00:00:00.000 | | | |-------------------------------------------------------------------------------------------------| """ def __init__(self, meta=None, timerange=None, colnames=None): self.metadata = [] # Parse in arguments if meta is not None: if (isinstance(meta, (dict, MetaDict)) and isinstance(timerange, TimeRange) and isinstance(colnames, list)): # Given a single metadata entry as a dictionary with additional timerange and colnames. self.metadata.append((timerange, colnames, meta)) elif isinstance(meta, tuple): # Given a single metadata entry as a tuple. if isinstance(meta[0], TimeRange) and isinstance( meta[1], list) and isinstance(meta[2], (dict, MetaDict)): self.metadata.append(meta) else: raise ValueError("Invalid parameters passed in the meta") elif isinstance(meta, list): # Given a complex metadata list (of tuples) for meta_tuple in meta: if isinstance(meta_tuple[0], TimeRange) and isinstance( meta_tuple[1], list) and isinstance( meta_tuple[2], (dict, MetaDict)): self.metadata.append(meta_tuple) else: raise ValueError( "Invalid parameters passed in the meta") else: # In the event no metadata dictionary is sent we default to something usable if isinstance(timerange, TimeRange): if isinstance(colnames, list): self.metadata.append((timerange, colnames, MetaDict())) else: self.metadata.append((timerange, [], MetaDict())) warnings.warn( "No time range given for metadata. " "This will mean the metadata can't be linked " "to columns in data.", SunpyUserWarning) else: raise ValueError("You cannot create a TimeSeriesMetaData " "object without specifying a TimeRange") def __eq__(self, other): """ Checks to see if two `~sunpy.timeseries.TimeSeriesMetaData` are the same, they have the same entries in the same order. Parameters ---------- other : `~sunpy.timeseries.metadata.TimeSeriesMetaData` The second `~sunpy.timeseries.metadata.TimeSeriesMetaData` to compare with. Returns ------- `bool` """ match = True if len(self.metadata) == len(other.metadata): for i in range(0, len(self.metadata)): if self.metadata[i] != other.metadata[i]: match = False else: match = False return match def __ne__(self, other): """ Checks to see if two `~sunpy.timeseries.TimeSeriesMetaData` are the not the same, they don't have same entries in the same order. Parameters ---------- other : `~sunpy.timeseries.TimeSeriesMetaData` The second `~sunpy.timeseries.TimeSeriesMetaData` to compare with. Returns ------- `bool` """ return not self == other def append(self, timerange, columns, metadata, **kwargs): """ Add the given metadata into the current metadata. Will add the new entry so the list is in chronological order. Parameters ---------- timerange : `~sunpy.time.TimeRange` The timerange for which a given metadict is relevant. This will generally initially be the full range of the original file, but if the TimeSeries gets truncated this may change appropriately. columns : `list` A list of the column name strings that the metadata is relevant for. metadata : `~sunpy.util.metadata.MetaDict` or `collections.OrderedDict` or `dict` The object holding the metadata. """ # Parameters metadata = MetaDict(metadata) # Check the types are correct. pos = 0 if isinstance(timerange, TimeRange): for i, meta in enumerate(self.metadata): if timerange.start > meta[0].start: pos = i + 1 else: raise ValueError( 'Incorrect datatime or data for append to TimeSeriesMetaData.') # Prepare tuple to append. new_metadata = (timerange, columns, metadata) # Check this isn't a duplicate entry (same TR and comnames) duplicate = False if pos < len(self.metadata): old_metadata = self.metadata[pos] if (new_metadata[0] == old_metadata[0]) and (new_metadata[1] == old_metadata[1]): duplicate = True # Insert into the given position if not duplicate: self.metadata.insert(pos, new_metadata) @add_common_docstring(**_variables_for_parse_time_docstring()) def find_indices(self, time=None, colname=None): """ Find the indices for all the metadata entries matching the given filters. Will return all metadata entry indices if no filters are given. Parameters ---------- time : {parse_time_types}, optional A `~sunpy.time.parse_time` parsable string that you need metadata for. Defaults to `None`. colname : `str`, optional A string that can be used to narrow results to specific columns. Defaults to `None`. Returns ------- `list` A list of integers that contain all matching metadata. """ # Parameters dt = time if not dt: dt = False elif isinstance(dt, str): dt = parse_time(dt) # Find all results with suitable timerange. results_indices = [] for i, meta in enumerate(self.metadata): if (not dt) or dt in meta[0]: results_indices.append(i) # Filter out only those with the correct column. results = [] for i in results_indices: if (colname in self.metadata[i][1]) or (not colname): results.append(i) return results @add_common_docstring(**_variables_for_parse_time_docstring()) def find(self, time=None, colname=None): """ Find all metadata matching the given filters. Will return all metadata entries if no filters are given. Parameters ---------- time : {parse_time_types}, optional A `~sunpy.time.parse_time` parsable string that you need metadata for. Defaults to `None`. colname : `str`, optional A string that can be used to narrow results to specific columns. Defaults to `None`. Returns ------- `~sunpy.timeseries.metadata.TimeSeriesMetaData` A `~sunpy.timeseries.metadata.TimeSeriesMetaData` that contain all matching metadata entries. """ # Get the indices indices = self.find_indices(time=time, colname=colname) # Extract the relevant metadata entries metadata = [] for i in indices: metadata.append(copy.copy(self.metadata[i])) # Return a TimeSeriesMetaData object return TimeSeriesMetaData(meta=metadata) def get_index(self, index): """ Return the dictionary entry at the given index. Parameters ---------- index : `int` The integer index of the metadata entry in the list. Returns ------- `~sunpy.util.metadata.MetaDict` An ordered Dictionary containing the metadata at the given index. """ return self.metadata[index][2] @add_common_docstring(**_variables_for_parse_time_docstring()) def get(self, keys, time=None, colname=None): """ Return a `~sunpy.timeseries.metadata.TimeSeriesMetaData` with all entries matching the filters which also contain the given input key. Parameters ---------- keys : `str` The Key/s to be searched in the dictionary. time : {parse_time_types}, optional A `~sunpy.time.parse_time` parsable string that you need metadata for. Defaults to `None`. colname : `str`, optional A string that can be used to narrow results to specific columns. Returns ------- metadata : `~sunpy.timeseries.metadata.TimeSeriesMetaData` A TimeSeriesMetaData that contain all matching metadata entries but with only the requested key/value pairs in the MetaDict objects. """ # Make a list of keys if only one is given if isinstance(keys, str): keys = [keys] # Find all metadata entries for the given time/colname filters full_metadata = self.find(time=time, colname=colname) metadata = [] # Append to metadata only key:value pairs with requested keys for i, entry in enumerate(full_metadata.metadata): metadict = MetaDict() for curkey, value in entry[2].items(): for key in keys: if curkey.lower() == key.lower(): metadict.update({key: value}) metadata.append((entry[0], entry[1], metadict)) # Return a TimeSeriesMetaData object return TimeSeriesMetaData(meta=metadata) def concatenate(self, others): """ Combine the metadata from a `~sunpy.timeseries.TimeSeriesMetaData` or an iterable containing multiple `~sunpy.timeseries.TimeSeriesMetaData` with the current `~sunpy.timeseries.TimeSeriesMetaData` and return it as a new `~sunpy.timeseries.TimeSeriesMetaData`. Parameters ---------- others : `~sunpy.timeseries.TimeSeriesMetaData` or `collections.abc.Iterable` The second `~sunpy.timeseries.metadata.TimeSeriesMetaData` object or an iterable containing multiple `~sunpy.timeseries.metadata.TimeSeriesMetaData` objects. """ # If an individual TimeSeriesMetaData object is to be concatenated, wrap it in a list # Else if it is an iterable, check if all items within it are valid # Else, data provided is invalid if isinstance(others, self.__class__): others = [others] elif isinstance(others, Iterable): if not all( isinstance(series, self.__class__) for series in others): raise TypeError( "Invalid type within iterable. Iterable must only contain " "TimeSeriesMetaData objects.") else: raise TypeError( f"Invalid type provided: {type(others)}. " "Please provide a TimeSeriesMetaData object or " "an iterable containing TimeSeriesMetaData objects.") # Create a copy of the metadata meta = TimeSeriesMetaData(copy.copy(self.metadata)) # Append each metadata entry of each TimeSeriesMetaData object from the iterable # to the original TimeSeriesMetaData object. for series in others: for entry in series.metadata: meta.append(entry[0], entry[1], entry[2]) return meta @add_common_docstring(**_variables_for_parse_time_docstring()) def update(self, dictionary, time=None, colname=None, overwrite=False): """ Updates the `~sunpy.timeseries.TimeSeriesMetaData` for all matching metadata entries. Parameters ---------- dictionary : `dict`, `collections.OrderedDict`, `~sunpy.util.metadata.MetaDict` The second `~sunpy.timeseries.TimeSeriesMetaData` object. time : {parse_time_types}, optional A `~sunpy.time.parse_time` parsable string that you need metadata for. Defaults to `None`. colname : `str`, optional A string that can be used to narrow results to specific columns. Defaults to `None`. overwrite : `bool`, optional Allows the user to overwrite already present keys. Defaults to `False` """ # Find all matching metadata entries indices = self.find_indices(time=time, colname=colname) # Now update each matching entries for i in indices: # Seperate keys for new and current pairs old_keys = set(dictionary.keys()) old_keys.intersection_update(set(self.metadata[i][2].keys())) new_keys = set(dictionary.keys()) new_keys.difference_update(old_keys) # Old keys only overwritten if allowed for key in (self.metadata[i][2].keys()): if key in old_keys and overwrite: self.metadata[i][2][key] = dictionary[key] for key in dictionary: if key in new_keys: self.metadata[i][2][key] = dictionary[key] def _truncate(self, timerange): """ Removes metadata entries outside of the new (truncated) `sunpy.time.TimeRange`. Also adjusts start and end times of time ranges going outside of the truncated time range. Parameters ---------- timerange : `sunpy.time.TimeRange` The time range to truncate to. """ truncated = [] for metatuple in self.metadata: # Get metadata time range parameters start = metatuple[0].start end = metatuple[0].end out_of_range = False # Find truncations if start < timerange.start and end > timerange.start: # Truncate the start start = timerange.start elif start > timerange.end: # Metadata time range starts after truncated data ends. out_of_range = True if end > timerange.end and start < timerange.end: # Truncate the end end = timerange.end elif end < timerange.start: # Metadata time range finishes before truncated data starts. out_of_range = True # Add the values if applicable if not out_of_range: truncated.append((TimeRange(start, end), metatuple[1], metatuple[2])) # Update the original list self.metadata = truncated @property def columns(self): """ Returns a list of all the names of the columns in the metadata. """ all_cols = set() for metatuple in self.metadata: all_cols.update(metatuple[1]) all_cols = list(all_cols) all_cols.sort() return all_cols @property def metas(self): """ Returns a list of all the metadict objects in the `~sunpy.timeseries.TimeSeriesMetaData`. """ all_metas = [] for metatuple in self.metadata: all_metas.append(metatuple[2]) return all_metas @property def timeranges(self): """ Returns a list of all the `~sunpy.time.TimeRange` in the `~sunpy.timeseries.TimeSeriesMetaData`. """ all_tr = [] for metatuple in self.metadata: all_tr.append(metatuple[0]) return all_tr def values(self): """ Returns a list of all the values from the metadict objects in each entry in the `~sunpy.timeseries.TimeSeriesMetaData`. """ all_vals = set() for metatuple in self.metadata: for key, value in metatuple[2].items(): all_vals.add(str(value)) all_vals = list(all_vals) all_vals.sort() return all_vals @property def time_range(self): """ Returns the `~sunpy.time.TimeRange` of the entire timeseries metadata. """ start = self.metadata[0][0].start end = self.metadata[0][0].end for metatuple in self.metadata: if end < metatuple[0].end: end = metatuple[0].end return TimeRange(start, end) def _remove_columns(self, colnames): """ Removes the given column(s) from the `~sunpy.timeseries.TimeSeriesMetaData`. Parameters ---------- colnames : `str`, `list` of `str` The name(s) of the columns to be removed. """ # Parameters if isinstance(colnames, str): colnames = [colnames] # Create a new list with all metadata entries without colnames reduced = [] for metatuple in self.metadata: # Check each colname for colname in colnames: if colname in metatuple[1]: # Removed from the list. metatuple[1].remove(colname) # Add the column if it still has some columns listed if len(metatuple[1]) > 0: reduced.append(metatuple) # Update the original list self.metadata = reduced def _rename_column(self, old, new): """ Change the name of a column in the metadata entries. Parameters ---------- old : `str` The original column name to be changed. new : `str` The new column name. """ for i in range(0, len(self.metadata)): # Update the colnames colnames = self.metadata[i][1] colnames = [w.replace(old, new) for w in colnames] # Replace values self.metadata[i] = (self.metadata[i][0], colnames, self.metadata[i][2]) def _validate_meta(self, meta): """ Validate a metadata argument. """ # Checking for metadata that may overlap. indices = range(0, len(self.metadata)) for i, j in itertools.combinations(indices, 2): # Check if the TimeRanges overlap if not ((self.metadata[i][0].end <= self.metadata[j][0].start) or (self.metadata[i][0].start >= self.metadata[j][0].end)): # Check column headings overlap col_overlap = list( set(self.metadata[i][1]) & set(self.metadata[j][1])) # If we have an overlap then show a warning if col_overlap: warnings.warn( f'Metadata entries {i} and {j} contain interleaved data.', SunpyUserWarning) # TODO: Check all entries are in tr.start time order. return True def to_string(self, depth=10, width=99): """ Print a table-like representation of the `~sunpy.timeseries.TimeSeriesMetaData`. Parameters ---------- depth : `int`, optional The maximum number of lines to show for each entry. Metadata dictionaries and column lists will be truncated if this is small. Defaults to 10. width : `int`, optional The number of characters wide to make the entire table. Defaults to 99. """ # Parameters colspace = ' | ' liswidths = (26, 15, width - 2 - 2 * len(colspace) - 26 - 15) colheadings = '|' + 'TimeRange'.ljust(100)[:liswidths[0]] + colspace colheadings += 'Columns'.ljust(100)[:liswidths[1]] + colspace colheadings += 'Meta'.ljust(100)[:liswidths[2]] + '|' rowspace = "-" * (liswidths[0] + len(colspace) + liswidths[1] + len(colspace) + liswidths[2]) rowspace = '|' + rowspace + '|' # Headings full = rowspace + '\n' + colheadings + '\n' + rowspace + '\n' # Add metadata entries for entry in self.metadata: # Make lists for each of the columns for each metadata entry # Padded to the widths given in liswidths lis_range = [ str(entry[0].start), ' to ', str(entry[0].end) ] # Shorten TimeRange representation if depth of only 2 if depth == 2: lis_range = [str(entry[0].start), str(entry[0].end)] liscols = [] for col in entry[1]: liscols.append(col.ljust(100)[:liswidths[1]]) lismeta = [] for key in list(entry[2].keys()): string = str(key) + ': ' + str(entry[2][key]) lismeta.append(string.ljust(100)[:liswidths[2]]) # Add lines of the entry upto the given depth for i in range(0, depth): # What to do in the event any of the lists have more entries # then the current depth if len(lis_range) > i or len(entry[1]) > i or len(lismeta) > i: # The start of the line Str is just a vertical bar/pipe line = '|' # Check we have a time range entry to print if len(lis_range) > i: # Simply add that time range entry to the line Str line += lis_range[i].ljust(100)[:liswidths[0]] else: # No entry to add, so just add a blank space line += ''.ljust(100)[:liswidths[0]] # Add a column break vertical bar/pipe line += colspace # Check we have another column name entry to print if len(entry[1]) > i: # Simply add that column name to the line Str line += entry[1][i].ljust(100)[:liswidths[1]] else: # No entry to add, so just add a blank space line += ''.ljust(100)[:liswidths[1]] # Add a column break vertical bar/pipe line += colspace # Check we have another meta key/value pair to print if len(lismeta) > i: # Simply add that key/value pair to the line Str line += lismeta[i].ljust(100)[:liswidths[2]] else: # No entry to add, so just add a blank space line += ''.ljust(100)[:liswidths[2]] # Finish the line Str with vertical bar/pipe and \n full += line + '|\n' # Reached the depth limit, add line to show if the columns are truncated if len(lis_range) >= depth or len( entry[1]) >= depth or len(lismeta) >= depth: # The start of the line Str is just a vertical bar/pipe line = '|' # Check we have more time range entries to print if len(lis_range) > depth: # We have more time range entries, use ellipsis to show this line += '...'.ljust(100)[:liswidths[0]] else: # No entry to add, so just add a blank space line += ''.ljust(100)[:liswidths[0]] # Add a column break vertical bar/pipe line += colspace # Check we have more than one column name entry to print if len(entry[1]) > depth: # We have more column name entries, use ellipsis line += '...'.ljust(100)[:liswidths[1]] else: # No more column name entries, so just add a blank space line += ''.ljust(100)[:liswidths[1]] # Add a column break vertical bar/pipe line += colspace # Check we have more meta key/value pairs to print if len(lismeta) > depth: # We have more key/value pairs, use ellipsis to show this line += '...'.ljust(100)[:liswidths[2]] else: # No morekey/value pairs, add a blank space line += ''.ljust(100)[:liswidths[2]] # Finish the line Str with vertical bar/pipe and \n full += line + '|\n' # Add a line to close the table full += rowspace + '\n' return full def __repr__(self): return self.to_string() def __str__(self): return self.to_string()
if i == 7: out.append('Auroral zone') if i == 8: out.append('Moon in LYRA') if i == 9: out.append('Moon in SWAP') if i == 10: out.append('Venus in LYRA') if i == 11: out.append('Venus in SWAP') return out # TODO: Change this function to only need the amount of channels to be passed in. @add_common_docstring(**_variables_for_parse_time_docstring()) def _prep_columns(time, channels=None, filecolumns=None): """ Checks and prepares data to be written out to a file. Parameters ---------- time: {parse_time_types} A list or array of times. channels: `list`, optional A list of the channels found within the data. filecolumns: `list`, optional A list of strings that are the column names for the file. Defaults to `None`, which means that a filenames list is generated equal to ``["time", "channel0", "channel1",..., "channelN"]`` where ``N`` is the number of arrays in the ``channels`` list (if provided).
class SUVIClient(GenericClient): """ Provides access to data from the GOES Solar Ultraviolet Imager (SUVI). SUVI data are provided by NOAA at the following url https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/ The SUVI instrument was first included on GOES-16. It produces level-1b as well as level-2 data products. Level-2 data products are a weighted average of level-1b product files and therefore provide higher imaging dynamic range than individual images. The exposure time of level 1b images range from 1 s to 0.005 s. SUVI supports the following wavelengths; 94, 131, 171, 195, 284, 304 angstrom. If no wavelength is specified, images from all wavelengths are returned. Note ---- GOES-16 began providing regular level-1b data on 2018-06-01. At the time of writing, SUVI on GOES-17 is operational but currently does not provide Level-2 data. """ @add_common_docstring(**_variables_for_parse_time_docstring()) def _get_goes_sat_num(self, date): """ Determines the best satellite number for a given date. Parameters ---------- date : {parse_time_types} The date to determine which satellite is active. Note ---- At the time this function was written. GOES-17 is operational but currently does not provide Level 2 data therefore it is never returned. The GOES-16 start date is based on the availability of regular level 1b data. """ # GOES-17 is operational but currently does not provide Level 2 data # GOES-16 start date is based on the availability of regular level 1b data suvi_operational = { 16: TimeRange("2018-06-01", parse_time("now")), } results = [] for sat_num in suvi_operational: if date in suvi_operational[sat_num]: # if true then the satellite with sat_num is available results.append(sat_num) if results: # Return the newest satellite return max(results) else: # if no satellites were found then raise an exception raise ValueError( f"No operational SUVI instrument on {date.strftime(TIME_FORMAT)}" ) def _get_time_for_url(self, urls): these_timeranges = [] for this_url in urls: if this_url.count('/l2/') > 0: # this is a level 2 data file start_time = parse_time( os.path.basename(this_url).split('_s')[2].split('Z')[0]) end_time = parse_time( os.path.basename(this_url).split('_e')[1].split('Z')[0]) these_timeranges.append(TimeRange(start_time, end_time)) if this_url.count('/l1b/') > 0: # this is a level 1b data file start_time = datetime.strptime( os.path.basename(this_url).split('_s')[1].split('_e')[0] [:-1], '%Y%j%H%M%S') end_time = datetime.strptime( os.path.basename(this_url).split('_e')[1].split('_c')[0] [:-1], '%Y%j%H%M%S') these_timeranges.append(TimeRange(start_time, end_time)) return these_timeranges def _get_url_for_timerange(self, timerange, **kwargs): """ Returns urls to the SUVI data for the specified time range. Parameters ---------- timerange: `sunpy.time.TimeRange` Time range for which data is to be downloaded. level : `str`, optional The level of the data. Possible values are 1b and 2 (default). wavelength : `astropy.units.Quantity` or `tuple`, optional Wavelength band. If not given, all wavelengths are returned. satellitenumber : `int`, optional GOES satellite number. Must be >= 16. Default is 16. """ base_url = "https://data.ngdc.noaa.gov/platforms/solar-space-observing-satellites/goes/goes{goes_number}/" supported_waves = [94, 131, 171, 195, 284, 304] supported_levels = ("2", "1b") # these are optional requirements so if not provided assume defaults # if wavelength is not provided assuming all of them if "wavelength" in kwargs.keys(): wavelength_input = kwargs.get("wavelength") if isinstance(wavelength_input, u.Quantity): # not a range if int(wavelength_input.to_value( 'Angstrom')) not in supported_waves: raise ValueError( f"Wavelength {kwargs.get('wavelength')} not supported." ) else: wavelength = [kwargs.get("wavelength")] else: # _Range was provided compress_index = [ wavelength_input.wavemin <= this_wave <= wavelength_input.wavemax for this_wave in (supported_waves * u.Angstrom) ] if not any(compress_index): raise ValueError( f"Wavelength {wavelength_input} not supported.") else: wavelength = list(compress(supported_waves, compress_index)) * u.Angstrom else: # no wavelength provided return all of them wavelength = supported_waves * u.Angstrom # check that the input wavelength can be converted to angstrom waves = [ int(this_wave.to_value('angstrom', equivalencies=u.spectral())) for this_wave in wavelength ] # use the given satellite number or choose the best one satellitenumber = int( kwargs.get("satellitenumber", self._get_goes_sat_num(timerange.start))) if satellitenumber < 16: raise ValueError( f"Satellite number {satellitenumber} not supported.") # default to the highest level of data level = str(kwargs.get( "level", "2")) # make string in case the input is a number if level not in supported_levels: raise ValueError(f"Level {level} is not supported.") results = [] for this_wave in waves: if level == "2": search_pattern = base_url + 'l{level}/data/suvi-l{level}-ci{wave:03}/%Y/%m/%d/dr_suvi-l{level}-ci{wave:03}_g{goes_number}_s%Y%m%dT%H%M%SZ_.*\.fits' elif level == "1b": if this_wave in [131, 171, 195, 284]: search_pattern = base_url + 'l{level}/suvi-l{level}-fe{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-Fe{wave:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz' elif this_wave == 304: search_pattern = base_url + 'l{level}/suvi-l{level}-he{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-He{wave_minus1:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz' elif this_wave == 94: search_pattern = base_url + 'l{level}/suvi-l{level}-fe{wave:03}/%Y/%m/%d/OR_SUVI-L{level}-Fe{wave_minus1:03}_G{goes_number}_s%Y%j%H%M%S.*\.fits.gz' if search_pattern.count('wave_minus1'): scraper = Scraper(search_pattern, level=level, wave=this_wave, goes_number=satellitenumber, wave_minus1=this_wave - 1) else: scraper = Scraper(search_pattern, level=level, wave=this_wave, goes_number=satellitenumber) results.extend(scraper.filelist(timerange)) return results def _makeimap(self): """ Helper Function used to hold information about source. """ self.map_['source'] = 'GOES' self.map_['provider'] = 'NOAA' self.map_['instrument'] = 'SUVI' self.map_['physobs'] = 'flux' @classmethod def _can_handle_query(cls, *query): """ Answers whether client can service the query. Parameters ---------- query : `tuple` All specified query objects. Returns ------- `bool` answer as to whether client can service the query. """ required = {a.Time, a.Instrument} optional = {a.Wavelength, a.Level, a.goes.SatelliteNumber} all_attrs = {type(x) for x in query} ops = all_attrs - required # check to ensure that all optional requirements are in approved list if ops and not all(elem in optional for elem in ops): return False # if we get this far we have either Instrument and Time # or Instrument, Time and Wavelength check_var_count = 0 for x in query: if isinstance(x, a.Instrument) and x.value.lower() == 'suvi': check_var_count += 1 if check_var_count == 1: return True else: return False
class TimeRange: """ A class to create and handle time ranges. .. note:: Regardless of how a `sunpy.time.TimeRange` is constructed it will always provide a positive time range where the start time is before the end time. `~sunpy.time.TimeRange.__contains__` has been implemented which means you can check if a time is within the time range you have created. Please see the example section below. Parameters ---------- a : {parse_time_types} A time (the start time) specified as a parse_time-compatible time string, number, or a datetime object. b : {parse_time_types} Another time (the end time) specified as a parse_time-compatible time string, number, or a datetime object. May also be the size of the time range specified as a timedelta object, or a `~astropy.units.Quantity`. Examples -------- >>> from sunpy.time import TimeRange >>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 00:20') >>> time_range = TimeRange(('2010/03/04 00:10', '2010/03/04 00:20')) >>> import astropy.units as u >>> time_range = TimeRange('2010/03/04 00:10', 400 * u.s) >>> TimeRange('2010/03/04 00:10', 400 * u.day) <sunpy.time.timerange.TimeRange object at ...> Start: 2010-03-04 00:10:00 End: 2011-04-08 00:10:00 Center:2010-09-20 00:10:00 Duration:400.0 days or 9600.0 hours or 576000.0 minutes or 34560000.0 seconds <BLANKLINE> >>> time1 = '2014/5/5 12:11' >>> time2 = '2012/5/5 12:11' >>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12') >>> time1 in time_range True >>> time2 in time_range False """ def __init__(self, a, b=None, format=None): # If a is a TimeRange object, copy attributes to new instance. self._t1 = None self._t2 = None if isinstance(a, TimeRange): self.__dict__ = a.__dict__.copy() return # Normalize different input types if b is None: x = parse_time(a[0], format=format) if len(a) != 2: raise ValueError('If b is None a must have two elements') else: y = a[1] else: x = parse_time(a, format=format) y = b if isinstance(y, u.Quantity): y = TimeDelta(y) if isinstance(y, timedelta): y = TimeDelta(y, format='datetime') # Timedelta if isinstance(y, TimeDelta): if y.jd >= 0: self._t1 = x self._t2 = x + y else: self._t1 = x + y self._t2 = x return # Otherwise, assume that the second argument is parse_time-compatible y = parse_time(y, format=format) if isinstance(y, Time): if x < y: self._t1 = x self._t2 = y else: self._t1 = y self._t2 = x @property def start(self): """ Get the start time. Returns ------- `astropy.time.Time` The start time. """ return self._t1 @property def end(self): """ Get the end time. Returns ------- `astropy.time.Time` The end time. """ return self._t2 @property def dt(self): """ Get the length of the time range. Always a positive value. Returns ------- `astropy.time.TimeDelta` The difference between the start and the end time. """ return self._t2 - self._t1 @property def center(self): """ Gets the center of the time range. Returns ------- `astropy.time.Time` The center time. """ return self.start + self.dt / 2 @property def hours(self): """ Get the number of hours elapsed. Returns ------- `astropy.units.Quantity` The amount of hours between the start and end time. """ return self.dt.to('hour') @property def days(self): """ Gets the number of days elapsed. Returns ------- `astropy.units.Quantity` The amount of days between the start and end time. """ return self.dt.to('d') @property def seconds(self): """ Gets the number of seconds elapsed. Returns ------- `astropy.units.Quantity` The amount of seconds between the start and end time. """ return self.dt.to('s') @property def minutes(self): """ Gets the number of minutes elapsed. Returns ------- `astropy.units.Quantity` The amount of minutes between the start and end time. """ return self.dt.to('min') def __eq__(self, other): """ Check that two `sunpy.time.TimeRange` have the same start and end datetime. Parameters ---------- other : `~sunpy.time.timerange.TimeRange` The second `sunpy.time.TimeRange` to compare to. Returns ------- `bool` `True` if equal, `False` otherwise. """ if isinstance(other, TimeRange): return is_time_equal(self.start, other.start) and is_time_equal( self.end, other.end) return NotImplemented def __ne__(self, other): """ Check two `sunpy.time.TimeRange` have different start or end datetimes. Parameters ---------- other : `~sunpy.time.timerange.TimeRange` The second `sunpy.time.TimeRange` to compare to. Returns ------- `bool` `True` if non-equal, `False` otherwise. """ if isinstance(other, TimeRange): return not (is_time_equal(self.start, other.start) and is_time_equal(self.end, other.end)) return NotImplemented def __repr__(self): """ Returns a human-readable representation of `sunpy.time.TimeRange`. """ t1 = self.start.strftime(TIME_FORMAT) t2 = self.end.strftime(TIME_FORMAT) center = self.center.strftime(TIME_FORMAT) fully_qualified_name = f'{self.__class__.__module__}.{self.__class__.__name__}' return (' <{} object at {}>'.format( fully_qualified_name, hex(id(self))) + '\n Start:'.ljust(12) + t1 + '\n End:'.ljust(12) + t2 + '\n Center:'.ljust(12) + center + '\n Duration:'.ljust(12) + str(self.days.value) + ' days or' + '\n '.ljust(12) + str(self.hours.value) + ' hours or' + '\n '.ljust(12) + str(self.minutes.value) + ' minutes or' + '\n '.ljust(12) + str(self.seconds.value) + ' seconds' + '\n') def split(self, n=2): """ Splits the time range into multiple equally sized parts. Parameters ---------- n : `int`, optional The number of times to split the time range (must >= 1). Defaults to 2. Returns ------- `list` A list of equally sized `sunpy.time.TimeRange` between the start and end times. """ if n <= 0: raise ValueError('n must be greater than or equal to 1') subsections = [] previous_time = self.start next_time = None for _ in range(n): next_time = previous_time + self.dt / n next_range = TimeRange(previous_time, next_time) subsections.append(next_range) previous_time = next_time return subsections def window(self, cadence, window): """ Split the time range up into a series of `~sunpy.time.TimeRange` that are ``window`` long, with a cadence of ``cadence``. Parameters ---------- cadence : `astropy.units.Quantity`, `astropy.time.TimeDelta` Cadence. window : `astropy.units.quantity`, `astropy.time.TimeDelta` The length of window. Returns ------- `list` A list of `~sunpy.time.TimeRange`, that are ``window`` long and separated by ``cadence``. Examples -------- >>> import astropy.units as u >>> from sunpy.time import TimeRange >>> time_range = TimeRange('2010/03/04 00:10', '2010/03/04 01:20') >>> time_range.window(60*60*u.s, window=12*u.s) # doctest: +SKIP [ <sunpy.time.timerange.TimeRange object at 0x7f0214bfc208> Start: 2010-03-04 00:10:00 End: 2010-03-04 00:10:12 Center:2010-03-04 00:10:06 Duration:0.0001388888888888889 days or 0.003333333333333333 hours or 0.2 minutes or 12.0 seconds, <sunpy.time.timerange.TimeRange object at 0x7f01fe43ac50> Start: 2010-03-04 01:10:00 End: 2010-03-04 01:10:12 Center:2010-03-04 01:10:06 Duration:0.0001388888888888889 days or 0.003333333333333333 hours or 0.2 minutes or 12.0 seconds, <sunpy.time.timerange.TimeRange object at 0x7f01fb90b898> Start: 2010-03-04 02:10:00 End: 2010-03-04 02:10:12 Center:2010-03-04 02:10:06 Duration:0.0001388888888888889 days or 0.003333333333333333 hours or 0.2 minutes or 12.0 seconds] """ # TODO: After astropy 3.1 remove this check if isinstance(window, timedelta): window = TimeDelta(window, format="datetime") if isinstance(cadence, timedelta): cadence = TimeDelta(cadence, format="datetime") if not isinstance(window, TimeDelta): window = TimeDelta(window) if not isinstance(cadence, TimeDelta): cadence = TimeDelta(cadence) n = 1 times = [TimeRange(self.start, self.start + window)] while times[-1].end < self.end: times.append( TimeRange(self.start + cadence * n, self.start + cadence * n + window)) n += 1 return times def next(self): """ Shift the time range forward by the amount of time elapsed. """ dt = self.dt self._t1 = self._t1 + dt self._t2 = self._t2 + dt return self def previous(self): """ Shift the time range backward by the amount of time elapsed. """ dt = self.dt self._t1 = self._t1 - dt self._t2 = self._t2 - dt return self def extend(self, dt_start, dt_end): """ Extend the time range forwards and backwards. Parameters ---------- dt_start : `astropy.time.TimeDelta` The amount to shift the start time. dt_end : `astropy.time.TimeDelta` The amount to shift the end time. """ # TODO: Support datetime.timedelta self._t1 = self._t1 + dt_start self._t2 = self._t2 + dt_end def get_dates(self): """ Return all partial days contained within the time range. """ delta = self.end.to_datetime().date() - self.start.to_datetime().date() dates = [] dates = [ parse_time(self.start.strftime('%Y-%m-%d')) + TimeDelta(i * u.day) for i in range(delta.days + 1) ] return dates @add_common_docstring(**_variables_for_parse_time_docstring()) def __contains__(self, time): """ Checks whether the given time lies within this range. Both limits are inclusive (i.e., ``__contains__(t1)`` and ``__contains__(t2)`` always return `True).that. Parameters ---------- time : {parse_time_types} {parse_time_desc} Returns ------- `bool` `True` if time lies between start and end, `False` otherwise. Examples -------- >>> from sunpy.time import TimeRange >>> time1 = '2014/5/5 12:11' >>> time2 = '2012/5/5 12:11' >>> time_range = TimeRange('2014/05/04 13:54', '2018/02/03 12:12') >>> time1 in time_range True >>> time2 in time_range False """ this_time = parse_time(time) return this_time >= self.start and this_time <= self.end
from datetime import timedelta import astropy.units as u from astropy.time import Time, TimeDelta from sunpy import config from sunpy.time import is_time_equal, parse_time from sunpy.time.time import _variables_for_parse_time_docstring from sunpy.util.decorators import add_common_docstring TIME_FORMAT = config.get('general', 'time_format') __all__ = ['TimeRange'] @add_common_docstring(**_variables_for_parse_time_docstring()) class TimeRange: """ A class to create and handle time ranges. .. note:: Regardless of how a `sunpy.time.TimeRange` is constructed it will always provide a positive time range where the start time is before the end time. Parameters ---------- a : {parse_time_types} A time (the start time) specified as a parse_time-compatible time string, number, or a datetime object. b : {parse_time_types}