Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multipart requests #46

Open
asyncee opened this issue Mar 19, 2019 · 38 comments
Open

Multipart requests #46

asyncee opened this issue Mar 19, 2019 · 38 comments
Milestone

Comments

@asyncee
Copy link

asyncee commented Mar 19, 2019

Hello!

I did not found standard way to describe a multipart request. Is it supported?

Currently i had to write something like this to let uploaded files work:

request_body_description = {
    "content": {
        "multipart/form-data": {
            "schema": {
                "type": "object",
                "properties": {"logo": {"type": "string", "format": "binary"}},
            }
        }
    }
}


@bp.route("/update-logo")
class UpdateLogo(MethodView):
    @bp.response(code=204)
    @bp.doc(requestBody=request_body_description)
    def post(self):
        file = flask.request.files["logo"]
        filename = secure_filename(file.filename)
        binary = file.read()
        
        ... do something with file ...

This code allows swagger to render input type="file" with name "logo" and send multipart request when executing request from web interface.

Am i missing something?

Thanks!

@toonalbers
Copy link

toonalbers commented Mar 27, 2019

I think this is a bug/unimplemented feature, although I have not had time to investigate it or report it properly.

Form data in OAS3 should be in the requestBody['content'] description as you have indicated, however form and file locations are currently mapped to a formData OpenApi location (which was correct for OAS2, but is not valid anymore).

I suspect it could be fixed here to also move formData locations to the requestBody. I'm not sure how that code would distinguish between multipart/form-data and application/x-www-form-urlencoded though.

@lafrech
Copy link
Member

lafrech commented Jul 12, 2019

Thanks for the report and investigation, and sorry for the delay.

https://swagger.io/docs/specification/describing-request-body/
https://swagger.io/docs/specification/describing-request-body/file-upload/

AFAIU, apispec's mapping is correct for OAS2. When using OAS3, files and form should be sent as requestBody, not parameters, so the mapping is not used (so files/form mapping is not really wrong in the OAS3 case, it is just useless).

However, what we do in flask-rest-api is a bit wrong. In arguments, we use the mapping from apispec to change parameters location. Then later on in _prepare_doc (at this stage, the doc has not been passed to apispec yet), we check for body and change that into requestBody if using OAS3 (so apispec never sees that body string and we could use any arbitrary string).

To maintain the OAS version abstraction, we should do the mapping in _prepare_doc. This way, in _prepare_doc, we'd have access to the string entered by the user before any mapping is done, and we'd do the right thing. In fact, we don't even need to do the mapping at all. This is apispec's job. All we need to do is ensure that when using OAS3, body/files/form are declared as requestBody.

Here's a PR implementing this: #81.


Now, we need to associate a content type to files and form. We may have to make an arbitrary choice here, just like we currently force application/json for body as it is complicated to parametrize.

What should the mapping be?

files: multipart/form-data? (And there would be no way to specify a specific type such as image/png)

form: multipart/form-data? application/x-www-form-urlencoded?

I'm thinking maybe we could modify arguments to allow passing the content type. It could be added to the temporary doc structure and used in _prepare_doc.

Guys, what do you think?

@toonalbers
Copy link

Hi, thanks for looking into this issue. I was actually looking at this again a few days ago.

PR #81 makes sense to me, when we later extend it for locations other than json.

Flask/webargs don't really care what content-type is set, right, so the cost for supporting more formats is not that high? I would definitely prefer being able to specify the content types. Right now I use application/x-www-form-urlencoded but there are plenty of use-cases for multipart/form-data so if it can be resolved per-case by the user I would appreciate it. Perhaps an optional content argument (same name as in OAS3) would do.

Having a default of multipart/form-data for both files and forms seems safest to me. (Consistent behavior of form data regardless of a file being present.)

@lafrech
Copy link
Member

lafrech commented Jul 15, 2019

Ho @toonalbers. Thanks for your help on this.

Here's a draft implementation: #83. It uses following mapping:

  • files: multipart/form-data
  • form: application/x-www-form-urlencoded

Those are default values than can be overridden in a Blueprint subclass

Next steps:

  • allow overriding content type par resource in @argument with content or content_type argument.
  • correctly set "consumes" in OAS2.

Note that the content type here is only used for documentation purpose. Using location=json, content_type="application/xml" won't make the application parse xml. To actually receive xml, the user would need to subclass FlaskParser to add a parse_xml method and complete the mappings with xml -> body (perhaps this could be added to flask-rest-api core, but I'd rather not add an external dependency for it, and I don't think it is such a common case). The form/files case is a bit simpler because webargs does nothing, it just injects the relevant part of the request into the view function, leaving it the job of actually reading the input.

@toonalbers
Copy link

Hi @lafrech. Looks good. Though, I'm wondering what a mixed form/file request (such as this) would look like with these default mappings. Would the result be something like this? I have not tried file requests myself, so maybe I'm also missing something in how the schema would be used.

"requestBody": {
  "content": {
    "application/x-www-form-urlencoded": {
      "schema": {
        "type": "object",
        "properties": {
          "orderId": { "type": "integer" },
          "userId": { "type": "integer" }
    } } },
    "multipart/form-data": {
      "schema": {
        "type": "object",
        "properties": {
          "fileName": {
            "type": "string",
            "format": "binary"
          }
    } } }
  }
}

I think the above would not be correct. So if this is the case, I would prefer just always having multipart/form-data. Or — with more effort — automatically changing to multipart/form-data if there are file parameters present. What do you think? Overriding the defaults would work around this in any case though, so thanks for that!

I agree with you on not adding more parsers yourself. If anyone should add it by default it would be webargs, like they do with JSON, but there's not much need.

@lafrech
Copy link
Member

lafrech commented Jul 15, 2019

Yes, I overlooked multipart. In fact, my implementation assumes there is only one request body parameter (only one parameter entered as json, form or files).

IIUC, when using multipart, there can be several parameters stuffed into the request, and those are documented as properties of the multipart content schema.

I think I understand your point. You're saying that if there is both files and form, form must be part of the multipart as well.

We'd need to merge mutipart parameters into the same multipart schema, rather than having 2 distincts multipart descriptions.

We could add some logic to check if there are multiple arguments in form/files so as to enforce multipart. It might be easier to give the user the ability (and responsibility) to provide the proper content type. But even then, maybe adding that logic would be useful to provide sensible defaults.

@toonalbers
Copy link

Ah, so it would not be as simple as I had hoped. Single file uploads and regular forms should work with #83 already, so that is nice. Merging arguments with the same location+content_type combination makes sense to me, would be a nice feature :)

@lafrech
Copy link
Member

lafrech commented Jul 15, 2019

Yes.

Just to be sure, IIUC, we'd even need to merge arguments with location form and files if they both have the multipart content type.

@toonalbers
Copy link

Yes, I think you are right.

@lafrech lafrech removed the question label Jul 16, 2019
@lafrech lafrech added this to the 1.0 milestone Jul 16, 2019
@lafrech
Copy link
Member

lafrech commented Aug 13, 2019

I pushed more commits in #83 to add the content_types argument to the arguments decorator. (Plural because the user may specify several types, for instance several image types.)

Feedback welcome.

This is almost good to go. Only the multipart case is not handled completely (see above). If it is too complicated to achieve, maybe I'll leave it for later, as a "known issue".

@ssfdust
Copy link

ssfdust commented Aug 21, 2019

Hello, I am willing to dive into the issue.
According to pallets/flask#2653, we need to implment our own form parser to parser the parts of form, and then put them into correct attribute of flask request, finally throw the request to webargs to parse, am I right?
I put a block of codes to make webargs(https://github.com/ssfdust/flask-rest-api/tree/form_files_request_body) to parse multiple locations according to schema, it works as expected. I don't know if this break principles of flask-rest-api, and I can't figure out a better way.

@lafrech
Copy link
Member

lafrech commented Aug 21, 2019

Thanks @ssfdust. That's interesting.

#83 is about correctly documenting resources. I'll limit its scope to that.

But of course, it's only useful if this lib doesn't get in the way when it comes to actually parsing a multipart request (not just documenting the resource). So in a further PR, we can do what is needed to allow that: do the necessary changes and either provide a helper or a piece of doc explaining how to do it in user code.

I'd need to take a deeper look at your implementation. Meanwhile, what you can do is try the branch in #83 and see what would be needed to correctly document the multipart resource. I think we'd need to merge the different parts location into a single multipart. That would happen in _prepare_doc.

@ssfdust
Copy link

ssfdust commented Aug 21, 2019

I'd like that. In fact, I'd like to achieve json data in multipart/form-data. So I create a decorator for the view function.
The decorator works by injecting the request instance before parsing data.
https://github.com/ssfdust/flask-jwt-app/blob/master/app/modules/storages/decorators.py
But to be a part of a framework, I think table-driven approach for every content type could be better.
The front end could package the json in their post data as a file, as described here.
https://stackoverflow.com/questions/50774176/sending-file-and-json-in-post-multipart-form-data-request-with-axios
In swagger a simple upload field, but only *.json is allowed is appreciate.

@lafrech
Copy link
Member

lafrech commented Sep 6, 2019

Good news. I finally found some time to sort this out.

Turns out multipart could be achieved nicely with very few code in this lib, relying on webargs/apispec features.

I merged #83 already (Fix doc of form and files arguments (content type, requestBody in OAS3), allow specifying content types).

The remaining work to support and document multipart relies on features in apispec that will be released (hopefully soon) in 3.0. Demonstration in https://github.com/Nobatek/flask-rest-api/tree/document_multipart_files.

Here's what it would look like.

        class MultipartSchema(ma.Schema):
            file_1 = Upload()
            file_2 = Upload()

        @blp.route('/', methods=['POST'])
        @blp.arguments(MultipartSchema, location='files')
        def func(files):
                print(files['file_1'].read().decode())
                print(files['file_2'].read().decode())
            )

        files = {
            'file_1': (io.BytesIO('Test 1'.encode()), 'file_1.txt'),
            'file_2': (io.BytesIO('Test 2'.encode()), 'file_2.txt'),
        }

        response = client.post('/test/', data=files)

The trick (which could also be seen as a limitation) is to declare a single Schema with a 'files' location.

I still need to properly test mixed content (file + other type in multipart) but I think we're close.

As usual, feedback welcome.

@lafrech
Copy link
Member

lafrech commented Sep 6, 2019

The code in the OP could read:

from flask_rest_api.fields import Upload

class MultipartSchema(ma.Schema):
    file = Upload()

@bp.route("/update-logo")
class UpdateLogo(MethodView):
    @bp.arguments(MultipartSchema, location='files')
    @bp.response(code=204)
    def post(self):
        file = files["logo"]
        filename = secure_filename(file.filename)
        binary = file.read()

@lafrech
Copy link
Member

lafrech commented Sep 6, 2019

IIUC, this won't work for file + other data, because when sending a multipart request with file + data, files end up in request.files while other data is in request.form (sounds obvious but took me some time to figure out...).

To actually get the data, one needs to put it in another Schema with location=form. And then, the multipart is not automagically documented. And we're back to #46 (comment)...

@lafrech
Copy link
Member

lafrech commented Sep 6, 2019

@ssfdust There's a huge rework going on in webargs and the location argument in field will be removed. Schemas will be deserialized from a singe location. You'll need one Schema per location.

@ssfdust
Copy link

ssfdust commented Sep 16, 2019

Got it. Sorry for delay, I am really busy these days.

@lafrech
Copy link
Member

lafrech commented Sep 16, 2019

I've been banging my head against this a little more. Unfortunately, auto-documenting mixed multipart request bodies is a bit complicated.

There are issues because for instance arguments allows to pass description, but this is a requestBody level attribute, not a part (as in multi-part) level attribute, therefore, what we expect from the following would be undetermined:

        @blp.arguments(FilesSchema, location='files', description='Some description')
        @blp.arguments(FormSchema, location='form', description='Some other description')

This implementation issues make me think that the sanest way to do that would be to require more explicitness from the user.

We could add a multipart_arguments decorator so that we don't have to detect multipart cases and we can ask for all needed parameters.

Things would go relatively smooth if we could pass apispec a schema that would be the aggregation of the different schemas involved ( a single schema inheriting from all). This could avoid duplicating apispec code, or having to pull functions from there to do the job here.

To be able to do so, we'd need to add a restriction to multipart_argument: only pass schema classes (or perhaps field dicts), not schema instances. Otherwise, modifiers would be lost in the process. I think we can live with this limitation.


I don't think I'll do that for the next release.

Once https://github.com/marshmallow-code/flask-smorest/tree/document_multipart_files is merged (almost done, just waiting for apispec 3.0), we'll be able to send files (using multipart), and to properly set a content type. This is already an improvement.

We shall add a note in the docs saying mixed multipart is not supported.

@zedrdave
Copy link

@lafrech Hey, just wanted to check on the status of this last part?
I am currently using a schema that has both files and data (using multipart), and while the following seems to work:

    @ns.arguments(myParameters, location='files')
    @ns.arguments(myParameters, location='form')
    def post(self, files, payload):

I wonder if there will be a way to do that in a cleaner way?

@max-gartz
Copy link

@lafrech
Hey, I am currently struggling with this issue in my project.
I am just not able to get this to work.
I need at least an id information together with the upload file. But when I add it in any location it does not work.
If I add the id in the same schema as the file, the values somehow are not transmitted.

Is there any solution or workaround available yet? Could you maybe provide an example for uploading a file with additional fields, so that the file can be associated with another entity.

Thank you very much!

@aprilahijriyan
Copy link

Any progress here? i am currently looking for a way how to work with this.

@ghostwriternr
Copy link

Can this issue be closed, considering #83 landed a while ago? (Great work, @lafrech : ))
@aprilahijriyan I stumbled upon this issue today too and looks like the behaviour is now well documented at File Upload. Tested on my project and it works perfectly fine.

@lafrech
Copy link
Member

lafrech commented Jun 21, 2021

Can this issue be closed, considering #83 landed a while ago?

Not really because the feature is not 100% complete.

As I wrote in #46 (comment)

mixed multipart is not supported

Yet, current implementation allows to upload files in a properly documented way so I don't expect to find the time to spend much effort on this anytime soon.

@claudius-kienle
Copy link

claudius-kienle commented Dec 15, 2022

I added following decorator. It's super ugly, but parses the form fields and generates the correct documentation:

def multipart_arguments(self,
                            schema,
                            *,
                            content_type=None,
                            required=True,
                            description=None,
                            example=None,
                            examples=None,
                            **kwargs):
        # At this stage, put schema instance in doc dictionary. Il will be
        # replaced later on by $ref or json.
        parameters = {
            'in': 'files',
            'required': required,
            'schema': schema,
        }
        if content_type is not None:
            parameters['content_type'] = content_type
        if example is not None:
            parameters['example'] = example
        if examples is not None:
            parameters['examples'] = examples
        if description is not None:
            parameters['description'] = description

        error_status_code = kwargs.get('error_status_code', self.ARGUMENTS_PARSER.DEFAULT_VALIDATION_STATUS)

        def decorator(func):

            arg_parser = self.ARGUMENTS_PARSER.use_args(schema, location='files', **kwargs)

            @wraps(func)
            def wrapper(*f_args, **f_kwargs):
                req = self.ARGUMENTS_PARSER.get_default_request()
                from werkzeug.datastructures import ImmutableMultiDict

                data = []
                orig_files = req.files
                orig_form = req.form
                for key in req.files:
                    data.append((key, req.files[key]))
                for key in req.form:
                    data.append((key, req.form[key]))
                req.form = None
                req.files = ImmutableMultiDict(data)

                try:
                    result = arg_parser(func)(*f_args, **f_kwargs)
                except Exception as e:
                    req.files = orig_files
                    req.form = orig_form
                    raise e

                req.files = orig_files
                req.form = orig_form

                return result

            # Add parameter to parameters list in doc info in function object
            # The deepcopy avoids modifying the wrapped function doc
            wrapper._apidoc = deepcopy(getattr(wrapper, '_apidoc', {}))
            docs = wrapper._apidoc.setdefault('arguments', {})
            docs.setdefault('parameters', []).append(parameters)
            docs.setdefault('responses', {})[error_status_code] = http.HTTPStatus(error_status_code).name

            # Call use_args (from webargs) to inject params in function
            return wrapper

        return decorator

@DineshGuptaa
Copy link

DineshGuptaa commented Dec 18, 2022

Hello All,
I am using the suggested code by lafrech

My code as below:

from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from werkzeug.utils import secure_filename
from flask import jsonify
from PIL import Image
import logging
blp = Blueprint("Aadhar", "Aadhar Card", description="Operations on Aadhar")

logger = logging.getLogger(__name__)
ALLOWED_EXTENSIONS = set(['pdf','png', 'jpg', 'jpeg', 'gif'])
from flask_smorest.fields import Upload
import marshmallow as ma

class MultipartSchema(ma.Schema):
	file = Upload()

@blp.route("/aadhar")
class Aadhar(MethodView):
	@blp.arguments(MultipartSchema, location='files')
	@blp.response(status_code=204)
	def post(self):
		logger.debug("Aadhar st method called...")
		file = files["logo"]
		filename = secure_filename(file.filename)
		binary = file.read()

and getting an error below:

Traceback (most recent call last):
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 2548, in __call__
	return self.wsgi_app(environ, start_response)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 2528, in wsgi_app
	response = self.handle_exception(e)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 2525, in wsgi_app
	response = self.full_dispatch_request()
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 1822, in full_dispatch_request
	rv = self.handle_user_exception(e)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 1820, in full_dispatch_request
	rv = self.dispatch_request()
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/app.py", line 1796, in dispatch_request
	return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/views.py", line 107, in view
	return current_app.ensure_sync(self.dispatch_request)(**kwargs)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask/views.py", line 188, in dispatch_request
	return current_app.ensure_sync(meth)(**kwargs)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/webargs/core.py", line 594, in wrapper
	return func(*args, **kwargs)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask_smorest/arguments.py", line 82, in wrapper
	return func(*f_args, **f_kwargs)
  File "/home/dinesh/pyworkspace/.VSMOREST/lib/python3.8/site-packages/flask_smorest/response.py", line 90, in wrapper
	func(*args, **kwargs)
TypeError: post() takes 1 positional argument but 2 were given

Please help me.

Thank you in advance.

@claudius-kienle
Copy link

@DineshGuptaa

You're missing a parameter in the post() method that will contain the data of the multipart schema.

So you have to replace

def post(self):

with

def post(self, data):

@DineshGuptaa
Copy link

DineshGuptaa commented Dec 18, 2022

@DineshGuptaa

You're missing a parameter in the post() method that will contain the data of the multipart schema.

So you have to replace

def post(self):

with

def post(self, data):

Please guide me on how may I send additional payload parameters. Also those parameters should be visible to the Swagger-UI

@DineshGuptaa
Copy link

Hi All,
I want to send some other parameters with a file. How may I achieve this task

from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from werkzeug.utils import secure_filename
import pytesseract, re
from flask import jsonify
from PIL import Image
import logging
import time
import os
blp = Blueprint("Aadhar", "Aadhar Card", description="Operations on Aadhar")

logger = logging.getLogger(__name__)
ALLOWED_EXTENSIONS = set(['pdf','png', 'jpg', 'jpeg', 'gif'])
from flask_smorest.fields import Upload
import marshmallow as ma

class MultipartSchema(ma.Schema):
	file = Upload()
	name = ma.fields.String()
	age= ma.fields.Integer()


@blp.route("/aadhar")
class Aadhar(MethodView):
	@blp.arguments(MultipartSchema, location="files")
	@blp.response(status_code=204)
	def post(self, data):
		logger.debug("Aadhar st method called...")
		file = data["file"]
		name = data["name"]
		age = data["name"]

I am getting only file param nothing else. Please suggest

Thanks in advance

@relyativist
Copy link

relyativist commented Mar 17, 2023

@DineshGuptaa
You won't be able to get file and form data from one schema, as Multipart requests with mixed types (file, form, etc.) are not supported. .
BTW, are there any updates on this issue?

@mzpqnxow
Copy link

mzpqnxow commented Apr 28, 2023

Based on the age of the issue (and what seems like a lot of effort from various folks along the way to "fix" this- quotes because I understand it's not necessarily broken as it is a limitation) is it safe to say that this is not going to happen? Perhaps this is generally somewhat of an anti-pattern for a true "restful" API, but there's no clean way around it in my case that doesn't involve adding quite a bit more complexity (breaking a single request into two requests- imagine creating a task with some k/v pairs, then having to PUT to that with the file contents- not ideal for the application or for the user)

@mzpqnxow
Copy link

mzpqnxow commented Sep 29, 2023

Following up / poking this as it's been a while now

You won't be able to get file and form data from one schema, as Multipart requests with mixed types (file, form, etc.) are not supported. .

It's not clear to me: is there a workaround to do this that is just a bit clunky, or is there no way to do it without throwing away some key benefits of using flask-smorest?

My users have a fixation on Swagger, and currently this issue means it will not have the file dialog for my mixed file/var endpoints- or it won't have the form vars. I want to ditch the docstring annotation-based solution I'm using now- it's bulky, requires being duplicated over and over for similar endpoints and provides no validation- but it allows a friendly Swagger UI with the form fields and the file parameters via a file upload dialog

It may not be the most common pattern, but unfortunately, most (almost all) of my endpoints fit it in this case 😔

I think curl is the easiest to succinctly describe it, if it's not obvious what I'm describing:

curl http://api.com/api/v1/submit -F [email protected] -F [email protected] -Fuuid=<uuid4> -Ffreetext_description_txt=<string>

I don't mean to ignore the docs, which are pretty clear on this, and appreciated:

Multipart requests with mixed types (file, form, etc.) are not supported. They can be achieved but the documentation is not correctly generated. arguments decorator can be called multiple times on the same view function but it should not be called with more that one request body location. This limitation is discussed in #46.

I also don't mean to ignore the prior discussion on the issue; I've read through it at least 3 times recently and a few times some months ago. But it's still not clear to me if this has been "fixed" or (if not) if it ever will be

I really want to use flask-smorest, but because so many of my endpoints are this way, it's not a one-off that I can deal with quickly by manually patching the generated specs, or by manually adding validation in one or two places; I mean, I could, but I'd rather not 😊

Sorry if the question isn't worth answering because nothing has changed, I'm misunderstanding, and/or I'm just rehashing the situation

Thanks in advance for any classification or recommendations for workarounds. I suppose I could simply move the non-file parameters to the query string, right? Can flask-smorest validate query string params in a POST? (I'll re-read the docs to answer that one myself)

Finally, thanks to the devs for this project, and to all, for the discussion on this issue. I sincerely appreciate it; and great job on the documentation with regard to this limitation. It saved me a ton of time second guessing my code when I first attempted to do this 😆

EDIT: My mistake for not considering the (possibly hacky) possibility of moving the text vars to query string params. I haven't done mixed query string/post body with flask/smorest, but perhaps that's a suitable way to do this?

@lafrech
Copy link
Member

lafrech commented Sep 29, 2023

Quick answer before I let this go.

It took me an important effort back then when implementing current solution with its limitations and now that I've put this aside for a while it would require an effort again just to remember/understand what the problem is. Plus that fact that I see it as a corner case : few users are affected, and this doesn't include myself... not much incentive to solve it.

I don't think anything has changed in flask-smorest that would make this simpler to achieve today.

Honestly, I didn't take the time to understand your specific use case (but it's nice you expose it here for other users that may be interested or for future reference). But I can at least answer that last one: yes, you may send query args in a POST. No pb.

Keeping this open for discussion, people are welcome to help solve this, share workarounds, etc.

@mzpqnxow
Copy link

Quick answer before I let this go.

Thanks for taking the time @lafrech, sincerely

It took me an important effort back then when implementing current solution with its limitations and now that I've put this aside for a while it would require an effort again just to remember/understand what the problem is

That's fair, and I appreciate your initial effort as well as this brief but thoughtful update, even if it's not a commitment to invest a ton of time into "fixing" for my use-case (which I think is actually a slight variation on the one you already spent time unwinding and solving)

Plus that fact that I see it as a corner case : few users are affected, and this doesn't include myself... not much incentive to solve it.

I tend to agree with your there, and I don't think it's even subjective; my thinking is that if it was a major/typical problem, there would be a lot more traffic on this issue

I'm grateful it still received the thought and effort that it did. I always find myself surprised at how genuinely helpful some projects/individuals are when it comes to this sort of thing. I always prefer a "I/we did what I/we could, it's all that can be done for now, time is finite, priorities are a thing, ..." over being completely ignored, or being made an insincere or careless promise that will never be fulfilled

I don't think anything has changed in flask-smorest that would make this simpler to achieve today.

👌

Honestly, I didn't take the time to understand your specific use case (but it's nice you expose it here for other users that may be interested or for future reference)

Yeah, I actually didn't realize that the original issue (I think?) was for multiple independently named multipart files, not a mixture of files and variables. Though are obviously very similar from an HTTP protocol level perspective, obviously it's not necessarily so when it comes to generating the OpenAPI "stuff," as I understand it. I'm not too deeply experienced or familiar with OpenAPI spec files but I did see the difference when I ended up annotated a few endpoints in-line in the docstring with OpenAPI YaML

But I can at least answer that last one: yes, you may send query args in a POST. No pb.

Ah that's quite helpful and I'm sorry for not having looked into that more deeply some time ago. Perhaps I need to just drop the "all arguments/variables must be in a POST" mentality for these endpoints, and use query params. What I wanted to avoid was making the endpoint path longer by embedding arguments into it, but for some stupid reason I had blocked out the (pretty standard practice) of using simple query parameters for those simple non-file variables. D'oh!

Keeping this open for discussion, people are welcome to help solve this, share workarounds, etc.

Thanks, would be great to see if anyone else has solved this in isolation and might be willing to contribute whatever they have. I'd be happy to re-base or bring it up to any standards required to merge

And for what it's worth, I do intend to submit a PR that aims to better accommodate this (unusual-ish) pattern at some point.

When I Have Time ™️

Either way I'll post back whatever I end up using, as you said, to potentially inform anyone else with the issue. The 3 or 4 of us that may be out there using this pattern 😆

Thanks again!

@ddorian
Copy link
Contributor

ddorian commented Sep 29, 2023

@mzpqnxow I just used this #46 (comment)

And it worked for me.

@lafrech The problem with using 2 arguments (1 for files & 1 for body) is that it breaks openapi-client-generator (fails to validate the spec).

@mzpqnxow
Copy link

@mzpqnxow I just used this #46 (comment)

And it worked for me.

@lafrech The problem with using 2 arguments (1 for files & 1 for body) is that it breaks openapi-client-generator (fails to validate the spec).

Just noticed that after I posted my message hah

I finished testing it a few minutes ago- not full testing, just ensuring my example produced what I expected- and came to report the same. It seems to work for what I need. The Swagger is nice and pretty with the clicky-clicky upload dialog as well as the non-file variables

I forked and then committed that single addition, in case anyone wants to try it without manually pasting it into the class in flask-smorest. It's in my master branch because I was too hasty to create a new branch (https://github.com/mzpqnxow/flask-smorest)

Regardless of where this may go (meaning, whether it will eventually reach a state where it is merged - assuming it's even desired), anyone with this issue that needs to install via pip, or via setuptools or pyproject.toml can do so using the syntax:

$ pip install 'git+https://github.com/mzpqnxow/flask-smorest@master#egg=flask-smorest'

Or, I've used the following in setup.cfg require:

flask-smorest @ 'git+https://github.com/mzpqnxow/flask-smorest@master#egg=flask-smorest

Thanks again all

@ddorian
Copy link
Contributor

ddorian commented Sep 29, 2023

anyone with this issue that needs to install via pip, or via setuptools or pyproject.toml can do so using the syntax:

People should just subclass the Blueprint and add the method, you probably won't maintain the fork.

@mzpqnxow
Copy link

mzpqnxow commented Oct 1, 2023

anyone with this issue that needs to install via pip, or via setuptools or pyproject.toml can do so using the syntax:

People should just subclass the Blueprint and add the method, you probably won't maintain the fork.

Ah yeah good point, a fork is a bit clunky and ridiculous when compared with a simple subclass. Thanks for suggesting this, I hadn't even thought about it

Regardless, I created #557 as a separate issue, to formally offer the functionality as a PR in principle

I'm happy with whatever is decided. I can keep a subclass in my project and not worry about goofy git+https:// syntax for require, and not worry about syncing a fork with upstream over time

Thanks again, not sure why I jumped right over that as one of the ways to work around this...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests