pyopenapi / pyswagger

An OpenAPI (fka Swagger) client & converter in python, which is type-safe, dynamic, spec-compliant.
MIT License
385 stars 89 forks source link

How can I conjure the parameters needed for execution of an API operation? #144

Open MartinDelVecchio opened 6 years ago

MartinDelVecchio commented 6 years ago

To date, I have been using pyswagger to execute API operations where I know the parameters that are specified, their types, which are required, etc. For example:

response = client.request (self.op['addEmployee'] (employee={'firstName': 'David', 'lastName': 'Pumpkins'}}

But now I want to programatically and generically generate these parameters, based on the definitions in the Swagger spec. And since Pyswagger has already interpreted the Swagger, I don't want to have to do it myself.

So in the above example, I would like to fetch the operation by ID:

op = self.op['addEmployee']

Then find somewhere in this operation object the guides needed for me to create the employee object. I would need to know that this operation takes one parameter, called 'employee', which is required. And that that object has two required parameters, both strings, called 'firstName' and 'lastName'. And also an optional age parameter, which is an integer, with an allowed range of 0 to 128. And also an optional group parameter, a string property with a default of 'none'.

With this information, I could build an employee object, which I could then pass to the operation execution:

response = op (employee=myEmployee)

Does pyswagger have any facility like this?

Thanks.

mission-liao commented 6 years ago

I've done something similar to what you described (refer to here). You can also refer to here if you need some detail to implement something similar on your own.

BTW, You can access each parameter by calling App.resolve and it structure is just following Swagger 2.0 definition. However, the definition of parameter would change when pyswagger upgrading its internal OpenAPI spec version to 3.0.0. So it's not recommended to design a tool based on parameters structure on your own.

Instead, a better way is defining a common interface to describe what you need, and re-implement the interface when upgrading OAI spec version. Therefore, if you have some thought about designing a tool to render requests automatically based on API spec from service provider, please feel to share your idea here.

MartinDelVecchio commented 6 years ago

Looking at this code:

# assume you have an Operation to test
input_ = renderer.render_all(
    app.s('user').post # the Operation
)

# this generated input could be passed to Operation.__call__,
# to get a pair of (Request, Response), or just
# pass them to client
resp = client.request(app.s('user').post(**input_))

I figured out from the source that renderer.render_all() expects a pyswagger Operation object, which I have for each operation I am trying to execute. So my code attempts to render a default set of parameters with:

    rendered = renderer.render_all (op)

This works for most operations, but not all. When it does not work, I get an error like this:

15:02:14  Error rendering api 'config' operation 'get-snapshot-by-time' parameters:  Traceback (most recent call last):
  File "/cygdrive/c/smoker/cgp/automation/src/python/smoky/smoky/cgp.py", line 464, in conjure_required_parameters
    rendered = renderer.render_all (op)
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 309, in render_all
    out.update({p.name: self.render(p, opt=opt)})
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 277, in render
    return self._generate(obj, opt=opt)
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 219, in _generate
    raise Exception('Unable to locate generator: {0}'.format(obj))
Exception: Unable to locate generator: <pyswagger.spec.v2_0.objects.Parameter object at 0x6fffbb1cf10>

In this example, the operation has two parameters, both required, and both in the path:

 paths:
  /status/cfg/snapshot/{domain}/time/{timestamp}:
    get:
      summary: Fetch the config snapshot specified by the time stamp
      description: Fetch the config snapshot specified by the time stamp
      operationId: get-snapshot-by-time
      tags:
      - Cfg
      parameters:
      - name: domain
        in: path
        type: string
        required: true
        description: Configuration domain (complete, system, etc.)
      - name: timestamp
        description: The time stamp (seconds since UTC epoch)
        in: path
        required: true
        type: integer

In another case, I get this error:

  File "/cygdrive/c/smoker/cgp/automation/src/python/smoky/smoky/cgp.py", line 464, in conjure_required_parameters
    rendered = renderer.render_all (op)
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 309, in render_all
    out.update({p.name: self.render(p, opt=opt)})
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 277, in render
    return self._generate(obj, opt=opt)
  File "/usr/lib/python2.7/site-packages/pyswagger/primitives/render.py", line 219, in _generate
    raise Exception('Unable to locate generator: {0}'.format(obj))
Exception: Unable to locate generator: <pyswagger.spec.v2_0.objects.Parameter object at 0x6fff9dd7810>

From a simpler operation:

paths:
  /:
    get:
      summary: Get the alarm
      description:  Get the system alarms
      operationId: get
      tags:
      - Alarms
      parameters:
      - name: page
        description: the page of results to get
        in: query
        type: integer
        default: 1
      - name: page_size
        description: the number of items for this page
        in: query
        type: integer
        default: 25

Can you tell what is going on here?

Thanks.

mission-liao commented 6 years ago

@MartinDelVecchio It's because the page_size parameter has type: integer but without format. There is a bug report and I didn't fix renderer part.

You can refer to here for detail and I would submit another fix after work tonight.

MartinDelVecchio commented 6 years ago

Ah yes, that was my bug!

I will work around it by adding format to my Swaggers. Then I can test your fix when it is ready.

Thanks.

mission-liao commented 6 years ago

@MartinDelVecchio I just submit a PR to address this issue, would be released once I finished Windows Support reported in another issue.

MartinDelVecchio commented 6 years ago

I am now getting an error when I use this option:

        # Generate all properties
        opt['max_property'] = True

It seems to want to generate values for read-only parameters, such as:

      serialNumber:
        type: string
        title: Serial Number
        description: The issuer of the certificates serial number
        readOnly: true

Is there a way to render all properties, except those marked with "readOnly: true"?

Thanks.

mission-liao commented 6 years ago

@MartinDelVecchio readOnly is not handled in pyswagger, would submit another PR for this.

MartinDelVecchio commented 6 years ago

I am also seeing problems with integer values, in which the selected random value does not meet the min/max criteria in the Swagger.

And it doesn't look like the default() map includes any limits on integer values.

MartinDelVecchio commented 6 years ago

And finally, I can't seem to have any influence on the values specified for nested-object parameters.

For example, if I have a simple parameter 'ServiceName', I am able to specify it by setting its value in the opt dictionary:

    parameter_template['ServiceName'] = 'MyServiceName'

But if my API has an object parameter 'MyObject', and that object has a property called 'ServiceName', it does not get my desired 'MyServiceName' setting. Nor does it work when I try to specify it this way:

    parameter_template['MyObject/ServiceName'] = 'MyServiceName'
    parameter_template['MyObject.ServiceName'] = 'MyServiceName'
    parameter_template['MyObject#ServiceName'] = 'MyServiceName'
mission-liao commented 6 years ago

@MartinDelVecchio The renderer I wrote didn't handle this case, reason:

It's still can be fixed to accept something described above (ex. parameter_template['MyObject/ServiceName'] = 'MyServiceName'). However, I'm spending my time to upgrade pyswagger to support OpenAPI 3.0.0 and have no time on this part. It's would be very helpful if you can submit a PR or to design a spec/use case/usage for a more flexible parameter render

MartinDelVecchio commented 6 years ago

OK, I understand. I was just hoping that I was missing something.

I will add a post-rendering step, in which I refine the object based on my knowledge of what should be there and what shouldn't.

Thanks.