-1

I am working on a script that let's me connect to the Sentinel satellite database to download the requested map files.

from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt

def get_available(input_geojson, user, password, date_start, date_end, satellite, sensormode, product_type):

    # LogIn
    api = SentinelAPI(user, password , 'https://scihub.copernicus.eu/dhus')

    # Input parameter of the search
    footprint = geojson_to_wkt(read_geojson(input_geojson))   # irrelevant to the question
    products = api.query(footprint,
                         date = (date_start, date_end),
                         platformname = satellite,
                         sensoroperationalmode = sensormode,
                         producttype = product_type,
                         )

My problem is depending on what kind of "satellite" input I am going to use will change what other arguments are necessary, required or even allowed. Some won't need "sensormode" and other maybe need "cloudcoverage". How would i go about writing a clean code with variable/optional arguments in a function within a function? Do I have to list every possible argument?

Ioannis Nasios
  • 8,292
  • 4
  • 33
  • 55
Schinken
  • 3
  • 2
  • 3
    What's with the "in a function within a function"? What is it that determines what arguments are needed? – martineau Nov 03 '20 at 23:24

3 Answers3

1

That api seems too tedious to use. Better group the arguments with classes.

from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt


class Satellite:
    def __init__(self, name, producttype, sensormode=None, cloudcoverage=None):
        self.name = name
        self.producttype = producttype
        self.sensormode = sensormode
        self.cloudcoverage = cloudcoverage


class SentinelConnection:
    def __init__(self, input_geojson, user, password):
        self.input_geojson = input_geojson
        self.user = user
        self.password = password
        self.api = None
        self.footprint = None

    def __enter__(self):
        self.api = SentinelAPI(self.user, self.password,
                               'https://scihub.copernicus.eu/dhus')
        self.footprint = geojson_to_wkt(read_geojson(self.input_geojson))

    def __exit__(self, exc_type, exc_val, exc_tb):
        # logout here
        pass


def get_available(conn, satellite, date_start, date_end):
    s = satellite
    products = conn.api.query(conn.footprint,
                              date=(date_start, date_end),
                              platformname=satellite,
                              sensoroperationalmode=s.sensormode,
                              producttype=s.product_type,
                              )


def main():
    with SentinelConnection("abc.json", "name", "password") as conn:
        satellite = Satellite('Sputnik X', 'military satellite')
        get_available(conn, satellite, date_start, date_end)

I have zero idea about what is a footprint. If different queries can use different footprint and the queries often reuse the same footprints, make a Location class for the foot print.

Crawl Cycle
  • 257
  • 2
  • 8
  • Thank you for your fast answer! I will look into your code. The footprint is just a geojson file of a polygon with the coordinates I want to get the satellite images from. – Schinken Nov 05 '20 at 05:29
1

I find Crawl Cycle's answer to be very Pythonic and beautiful, so I recommend going with that. Regardless, I had fun working on this so here's my interpretation of what you were looking for :)

import inspect


def foo_api(*, foo=None):
    print(f'foo_api: foo={foo}')

def bar_api(*, bar=None):
    print(f'bar_api: bar={bar}')

_API_BY_PARAMETERS = {
    frozenset(inspect.signature(api).parameters): api
    for api in (foo_api, bar_api)
}


def api(**kwargs):
    """Selects the correct API to call based on the given kwargs."""
    actual_params = frozenset(kwargs)
    if actual_params in _API_BY_PARAMETERS:
        actual_api = _API_BY_PARAMETERS[actual_params]
        return actual_api(**kwargs)
    else:
        param_errors = (
            (api.__name__,
             ', '.join(sorted(expected_params - actual_params)),
             ', '.join(sorted(actual_params - expected_params)))
            for expected_params, api in _API_BY_PARAMETERS.items())
        raise TypeError(
            'Arguments must match exactly with one of the APIs, but found '
            'the following differences: ' +
            '; '.join(
                f'{api_name} -> missing=[{missing}], unexpected=[{unexpected}]'
                for api_name, missing, unexpected in param_errors))

Try it Online

There are a few constraints that keep this implementation as concise as it is:

  • All API signatures must be unique.
  • All API signatures accept keyword arguments only.
Brian Rodriguez
  • 4,250
  • 1
  • 16
  • 37
0

I believe this would be solved well by **kwargs. This allows you to pass any keywork argument to a function without specifying then in the function signature ie:

def foobar(foo, **kwargs):
   bar = kwargs["bar"]
   return foo + bar


barfoo = foobar(foo='something', bar='other_thing')
  • Thank you for your answer! Using **kwargs seems to be the simplest option. I created a dictionary outside the function and will just give this as an **kwarg to the function to be used in the API. seems to be working so far – Schinken Nov 05 '20 at 05:30