.. _remote_details: Remote Data Access ================== .. _sdssclient: Sending HTTP Requests with SDSSClient ------------------------------------- All http requests are sent using the `~sdss_brain.api.client.SDSSClient`, which is a convenience wrapper class around the `httpx `_ python package for sending requests. ``httpx`` is a modern request framework aiming to mirror the API of the `requests `_ python package. ``httpx`` also provides built-in async support. See `~sdss_brain.api.client.SDSSAsyncClient` for the async version of the remote client. The main advantage of using the ``SDSSClient`` is integration with SDSS APIs and SDSS user authentication, although it can be used with any explicit url. Let's submit a "hello world" request to the Marvin API using the public domain `dr15.sdss.org`. :: >>> from sdss_brain.api.client import SDSSClient >>> # load the client to use the marvin API on public domain DR15, >>> # accessing the "cubes hello world" url route >>> s = SDSSClient('cubes', use_api='marvin', domain='dr15') >>> s >>> # check the full url >>> s.url 'https://dr15.sdss.org/marvin/api/cubes' When we instantiate the client with an API and a domain name, the correct base url is constructed behind the scenes. Any input url is then treated as a route segment to be appended to the base url. The fully constructed url can be shown with the ``url`` attribute. We can now send the request. ``request`` is a convenience wrapper for sending ``httpx`` get, post, or stream requests. Let's send the request as default without any parameters. :: >>> # send the http request >>> s.request() When requests are successful, the response content is extracted into the ``data`` attribute. If the request is not successful, an ``httpx.HttpStatusError`` will be raised. >>> # access the returned data >>> s.data {'data': 'this is a cube!', 'error': None, 'inconfig': {'release': 'DR16'}, 'status': 1, 'traceback': None} To send requests to different urls on a single API, you can pass an explicit url or route segment into the ``request`` method instead of during client instantiation. Let's send a public request to retrieve cube information for MaNGA galaxy "8485-1901" on release DR15. :: >>> # load the client with the proper API >>> s = SDSSClient(use_api='marvin', domain='dr15', release='DR15') >>> # send a POST request >>> s.request('cubes/8485-1901/', method='post') The full url is ``https://dr15.sdss.org/marvin/api/cubes/8485-1901/`` but we only need to input the portion of the url relative to the base url, often referred to as "route", "segment", or "path". We can access the response data as before. In this case, the response is a dictionary with galaxy metadata contained in a "data" key. :: >>> # access the data >>> s.data {'data': {'dec': 48.6902009334, 'header': ..., 'mangaid': '1-209232', 'plateifu': '8485-1901', 'ra': 232.544703894, 'redshift': 0.040744692, 'shape': [34, 34], 'wavelength': [3621.6, 3622.43, ..], ... } 'error': None, 'status': 1 } >>> # access the manga ID, the RA and Dec, and redshift >>> s.data['data']['mangaid'], s.data['data']['ra'], s.data['data']['dec'], s.data['data']['redshift'] ('1-209232', 232.544703894, 48.6902009334, 0.040744692) The underlying http response is available in the ``response`` attribute. :: >>> # access the httpx response >>> s.response For direct access to the ``httpx`` client, use the ``client`` attribute. :: >>> # access the raw httpx client or async client >>> s.client By default, the ``SDSSClient`` uses a generic "sdss" user; see :ref:`auth` for more information. :: >>> # look at the user attached to the client >>> s.user .. _apim: The Api Manager --------------- ``sdss_brain`` provides an API manager (`~sdss_brain.api.manager.ApiManager`) for seeing the available SDSS domains and APIs for remotely accessing data. :: >>> # load the API manager >>> from sdss_brain.api.manager import apim >>> apim You can list all the available domains used by SDSS. :: >>> # list the domains >>> apim.list_domains() [Domain(name='data.sdss.org', public=False, description='domain for accessing SDSS data on the SAS'), Domain(name='sas.sdss.org', public=False, description='domain for accessing various SAS services'), Domain(name='api.sdss.org', public=False, description='domain for accessing SDSS APIs'), Domain(name='lore.sdss.utah.edu', public=False, description='domain for accessing internal content on SDSS host lore'), Domain(name='internal.sdss.org', public=False, description='domain for accessing internal SDSS information'), Domain(name='magrathea.sdss.org', public=False, description="mirror domain for SDSS services, e.g. SDSS MaNGA's Marvin"), Domain(name='dr15.sdss.org', public=True, description='public domain for DR15 data access'), Domain(name='dr16.sdss.org', public=True, description='public domain for DR16 data access'), Domain(name='localhost', public=False, description='domain when running services locally')] or you can list the available APIs. :: >>> # list the APIs >>> apim.list_apis() [, , ] APIs can be accessed on the ``apis`` attribute. :: >>> # access the available APIs >>> apim.apis {'marvin': , 'icdb': , 'valis': } >>> # select the marvin API >>> apim.apis['marvin'] Each list of domains or apis can also be rendered as an Astropy `~astropy.table.Table`, with `~sdss_brain.api.manager.ApiManager.display`. :: >>> # display the available domains as a table >>> apim.display('domains') key name public description str9 str18 bool str57 --------- ------------------ ------ --------------------------------------------------------- data data.sdss.org False domain for accessing SDSS data on the SAS sas sas.sdss.org False domain for accessing various SAS services api api.sdss.org False domain for accessing SDSS APIs lore lore.sdss.utah.edu False domain for accessing internal content on SDSS host lore internal internal.sdss.org False domain for accessing internal SDSS information magrathea magrathea.sdss.org False mirror domain for SDSS services, e.g. SDSS MaNGA's Marvin dr15 dr15.sdss.org True public domain for DR15 data access dr16 dr16.sdss.org True public domain for DR16 data access local localhost False domain when running services locally Displaying the API information will also include any links to API documentation that exists for the given API. :: >>> apim.display('apis')
key base description ... auth docs str6 str13 str48 ... str5 object ------ ------------- ------------------------------------------------ ... ----- --------------------------------------------------------------- marvin marvin API for accessing MaNGA data via Marvin ... token https://sdss-marvin.readthedocs.io/en/stable/reference/web.html icdb collaboration API for accessing SDSS collaboration information ... netrc None valis valis API for SDSS data access ... netrc None The ``ApiManager`` also provides a mechanism for identifying an API and domain given a url string. :: >>> # attempt to identify a domain and API >>> apim.identify_api_from_url('https://dr15.sdss.org/marvin/api') ('marvin', 'dr15') .. _apiprofile: The Api Profile --------------- Just as ``sdssdb`` database profiles in ``sdssdb.yml`` define connections to different SDSS databases and map to `~sdssdb.connection.DatabaseConnection` objects, API profiles defined in ``api_profiles.yml`` define connections to available SDSS APIs, and map to `~sdss_brain.api.manager.ApiProfile` objects. Each API profile carries with it a list of domains and/or mirrors the API can be accessed on, any authentication type needed for access, and the currently constructed root or base url for accessing content on that API. The following examples are written using the MaNGA Marvin API, but the same applies to any other API. By default, an API is set to use the first domain in the list of domains, and will construct the base url for the API on that domain. :: >>> # access the "marvin" API profile >>> from sdss_brain.api.manager import ApiProfile >>> prof = ApiProfile('marvin') >>> prof You can view the available domains. :: >>> # display the list of available domains >>> prof.domains {'sas': Domain(name='sas.sdss.org', public=False, description='domain for accessing various SAS services'), 'lore': Domain(name='lore.sdss.utah.edu', public=False, description='domain for accessing internal content on SDSS host lore'), 'dr15': Domain(name='dr15.sdss.org', public=True, description='public domain for DR15 data access'), 'dr16': Domain(name='dr16.sdss.org', public=True, description='public domain for DR16 data access'), 'local': Domain(name='localhost', public=False, description='domain when running services locally')} You can change the domain the API uses to any other available one. Let's change to the DR15 domain. :: >>> # change to dr15.sdss.org >>> prof.change_domain('dr15') >>> prof The base url has now been updated. For development, we often set up a system on a localhost domain. ``localhost`` domains require a port number or ngrok id to be given as input. `ngrok `_ is a service used for opening up local web servers publicly. :: >>> # change to localhost domain on port 5000 >>> prof.change_domain('local', port=5000) >>> prof.url 'http://localhost:5000/marvin/api' >>> # change to localhost domain served with ngrok >>> prof.change_domain('local', ngrokid=12345) >>> prof.url 'http://12345.ngrok.io/marvin/api' Often APIs have test sites available to check changes and new features before pushing to production. These paths can be accessed with the ``change_path`` method. Setting the ``test=True`` keyword switches the path to the designated test site. Calling ``change_path`` without arguments sets the API to its production path. :: >>> # change back to the production site on the SAS domain >>> prof.change_domain('sas') >>> prof.url 'https://sas.sdss.org/marvin/api' >>> # change to the test site >>> prof.change_path(test=True) >>> prof.url 'https://sas.sdss.org/test/marvin/api' >>> # switch back to production >>> prof.change_path() >>> prof.url 'https://sas.sdss.org/marvin/api' The ``url`` attribute always defines the base url to the top level API. To build urls that point to specific routes on the API, use the ``construct_route`` method. Let's construct a new url to access the cube data for MaNGA galaxy "8485-1901". :: >>> # build a new url to a specific known API route >>> prof.construct_route('cubes/8485-1901/') 'https://sas.sdss.org/marvin/api/cubes/8485-1901/' Defining a new API Profile -------------------------- Once a new API has been built, to make it available to ``sdss_brain``, a new profile must be created in the ``python/sdss_brain/etc/api_profiles.yml`` YAML file in the `sdss_brain Github repo `_. A new API is defined using the following schema: :: schema: base: the base name of the API. Required. domains: the domains where the API is active. Required. description: a brief description of the API purpose docs: a url link to any API documentation mirrors: the domains for possible mirrors stems: path stems to denote test or alternate servers test: the name of the development stem. affix: whether the alternate base is a prefix or suffix api: whether the API is under an "api" stem routemap: an API route relative to the base url that returns the available routes on the given API auth: the type of authentication the API needs for non-public APIs type: whether the auth is netrc or token route: the API route relative to the base url to use for retrieving a token Only the first two items, ``base`` and ``domains`` are required entries. As an example, let's create an entry for a fake API called "infoviz" available on domains "sas.sdss.org" and "dr15.sdss.org". It also has a test server located at ``sas.sdss.org/dev/infoviz``. Our profile entry would like that: :: apis: info: base: infoviz domains: - sas - dr15 stems: test: dev affix: prefix An `~sdss_brain.api.manager.ApiProfile` is automatically constructed and is made accessible via the `~sdss_brain.api.manager.ApiManager`. :: >>> from sdss_brain.api.manager import apim >>> # load our new infoviz API >>> info = apim.apis['info'] >>> info >>> # access the test server >>> info.change_path(test=True) >>> info We can now construct urls to access specific routes on this API. :: >>> info.construct_route('/getinfo/here/') 'https://sas.sdss.org/dev/infoviz/getinfo/here/' Setting a global Url or API --------------------------- In the rare case that you want to use a single API for all ``Brain``-based tools, you can set one on the global config object using the `~sdss_brain.config.Config.set_api`. This will set an API to your global config, and all tools will use this global API. ``config.apis`` contains an instance of the `~sdss_brain.api.manager.ApiManager`. :: >>> from sdss_brain.config import config >>> # look up the set profile in the API manager >>> config.apis.profile None >>> # set a global API config.set_api('marvin') >>> config.apis.profile You can also set one permanently by setting the ``default_api`` argument in your ``~/.config/sdss/sdss_brain.yml`` config file.