Repository: obspy/obspy

Issue:
fdsn.client: add query parameters to get_station_bulk()
### Problem

The get_station_bulk() method implements the FDSNWS station POST query requests. However, while the standard supports various query parameters, get_station_bulk() currently supports only a subset as key=value pairs at the start of the request body:  level, includerestriced (typo!), includeavailability.

### Proposed solution

Add query parameters supported by the standard and/or the service endpoint. 

Some extra complexity is caused by the fact that most parameters are only "optional" and the specs are not very clear about the set of supported parameters. For GET method the set of supported parameters is declared by  `application.wadl`. It is used by ObsPy to discover available parameters for an endpoint. For POST this seems not to apply and it is not clear if we can assume that the same parameters are supported by GET and POST request.

* get_station_bulk() should support at least all "required" parameters, i.e. minlatitude, maxlatitude, minlongitude, maxlongitude;

* Optional parameters includerestricted and includeavailability are already available, though they might not be supported by the service endpoint because "optional";

* format and nodata (optional) seem particularly useful.
 


Discussion:
I'm probably wrong but I assumed this function was expressly for downloading a list of _known_ stations/channels, in which case having the extra search parameters is redundant (e.g. you would just use `get_stations`). Also I don't think it's an issue to make include restricted/availability mandatory as every server should have a default (NO) for this already. `Format` should probably added though as currently the method can only save as xml from the looks of it. 

typo is line 1185 of `clients/fdsn/client.py` but it looks like that's been fixed

i can push a PR in a sec... https://github.com/obspy/obspy/pull/3173
> Some extra complexity is caused by the fact that most parameters are only "optional" and the specs are not very clear about the set of supported parameters. For GET method the set of supported parameters is declared by `application.wadl`. It is used by ObsPy to discover available parameters for an endpoint. For POST this seems not to apply and it is not clear if we can assume that the same parameters are supported by GET and POST request.

I think we will just have to assume that the server will return a useful error message if one of the optional parameters is not accepted?
> typo is line 1185 of `clients/fdsn/client.py` but it looks like that's been fixed

yes the typo is fixed, overlooked this issue here
> in which case having the extra search parameters is redundant (e.g. you would just use `get_stations`

I would just argue to follow the FDSNWS specs to the letter, and it says all parameters are supposed to be put into the POST data part.

![Screenshot from 2022-10-11 15-48-58](https://user-images.githubusercontent.com/1842780/195114488-9664d9e7-78de-40d1-9f2b-c3402abc2675.png)


Resolving patch:
diff --git a/obspy/clients/fdsn/client.py b/obspy/clients/fdsn/client.py
--- a/obspy/clients/fdsn/client.py
+++ b/obspy/clients/fdsn/client.py
@@ -752,7 +752,12 @@ def get_stations(self, starttime=None, endtime=None, startbefore=None,
             data_stream.close()
         else:
             # This works with XML and StationXML data.
-            inventory = read_inventory(data_stream)
+            if format is None or format == 'xml':
+                inventory = read_inventory(data_stream, format='STATIONXML')
+            elif format == 'text':
+                inventory = read_inventory(data_stream, format='STATIONTEXT')
+            else:
+                inventory = read_inventory(data_stream)
             data_stream.close()
             return inventory
 
@@ -1058,8 +1063,13 @@ def get_waveforms_bulk(self, bulk, quality=None, minimumlength=None,
             return st
 
     def get_stations_bulk(self, bulk, level=None, includerestricted=None,
-                          includeavailability=None, filename=None, **kwargs):
-        r"""
+                          includeavailability=None, filename=None,
+                          minlatitude=None, maxlatitude=None,
+                          minlongitude=None, maxlongitude=None, latitude=None,
+                          longitude=None, minradius=None, maxradius=None,
+                          updatedafter=None, matchtimeseries=None, format=None,
+                          **kwargs):
+        """
         Query the station service of the client. Bulk request.
 
         Send a bulk request for stations to the server. `bulk` can either be
@@ -1156,6 +1166,31 @@ def get_stations_bulk(self, bulk, level=None, includerestricted=None,
         :type bulk: str, file or list[list]
         :param bulk: Information about the requested data. See above for
             details.
+        :type minlatitude: float
+        :param minlatitude: Limit to stations with a latitude larger than the
+            specified minimum.
+        :type maxlatitude: float
+        :param maxlatitude: Limit to stations with a latitude smaller than the
+            specified maximum.
+        :type minlongitude: float
+        :param minlongitude: Limit to stations with a longitude larger than the
+            specified minimum.
+        :type maxlongitude: float
+        :param maxlongitude: Limit to stations with a longitude smaller than
+            the specified maximum.
+        :type latitude: float
+        :param latitude: Specify the latitude to be used for a radius search.
+        :type longitude: float
+        :param longitude: Specify the longitude to be used for a radius
+            search.
+        :type minradius: float
+        :param minradius: Limit results to stations within the specified
+            minimum number of degrees from the geographic point defined by the
+            latitude and longitude parameters.
+        :type maxradius: float
+        :param maxradius: Limit results to stations within the specified
+            maximum number of degrees from the geographic point defined by the
+            latitude and longitude parameters.
         :type level: str
         :param level: Specify the level of detail for the results ("network",
             "station", "channel", "response"), e.g. specify "response" to get
@@ -1166,10 +1201,20 @@ def get_stations_bulk(self, bulk, level=None, includerestricted=None,
         :type includeavailability: bool
         :param includeavailability: Specify if results should include
             information about time series data availability.
+        :type updatedafter: :class:`~obspy.core.utcdatetime.UTCDateTime`
+        :param updatedafter: Limit to metadata updated after specified date;
+            updates are data center specific.
+        :type matchtimeseries: bool
+        :param matchtimeseries: Only include data for which matching time
+            series data is available.
         :type filename: str or file
         :param filename: If given, the downloaded data will be saved there
             instead of being parsed to an ObsPy object. Thus it will contain
             the raw data from the webservices.
+        :type format: str
+        :param format: The format in which to request station information.
+            ``"xml"`` (StationXML) or ``"text"`` (FDSN station text format).
+            XML has more information but text is much faster.
 
         Any additional keyword arguments will be passed to the webservice as
         additional arguments. If you pass one of the default parameters and the
@@ -1182,9 +1227,20 @@ def get_stations_bulk(self, bulk, level=None, includerestricted=None,
             raise ValueError(msg)
 
         arguments = OrderedDict(
+            minlatitude=minlatitude,
+            maxlatitude=maxlatitude,
+            minlongitude=minlongitude,
+            maxlongitude=maxlongitude,
+            latitude=latitude,
+            longitude=longitude,
+            minradius=minradius,
+            maxradius=maxradius,
             level=level,
             includerestricted=includerestricted,
-            includeavailability=includeavailability
+            includeavailability=includeavailability,
+            updatedafter=updatedafter,
+            matchtimeseries=matchtimeseries,
+            format=format
         )
         bulk = get_bulk_string(bulk, arguments)
 
@@ -1199,7 +1255,12 @@ def get_stations_bulk(self, bulk, level=None, includerestricted=None,
             return
         else:
             # Works with text and StationXML data.
-            inv = obspy.read_inventory(data_stream)
+            if format is None or format == 'xml':
+                inv = read_inventory(data_stream, format='STATIONXML')
+            elif format == 'text':
+                inv = read_inventory(data_stream, format='STATIONTEXT')
+            else:
+                inv = read_inventory(data_stream)
             data_stream.close()
             return inv
 


Test patch: