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

Feature request: Support application-factory pattern for Python apps #1106

Closed
bunny-therapist opened this issue Jan 31, 2024 · 18 comments · Fixed by #1336
Closed

Feature request: Support application-factory pattern for Python apps #1106

bunny-therapist opened this issue Jan 31, 2024 · 18 comments · Fixed by #1336
Labels
L-Python T-Enhancement New features and user-facing improvements z-python Language-Module for Python

Comments

@bunny-therapist
Copy link

"Application factory" means that the "app" object in your python code that you are passing to unit is not the app itself, but a callable that needs to be called to create the app, i.e. the_actual_app = app().

Uvicorn supports this with the --factory option: https://www.uvicorn.org/settings/
Gunicorn also supports this by specifying the app with parentheses: "app()": https://flask.palletsprojects.com/en/3.0.x/deploying/gunicorn/#running

It would be nice if unit supported this for Python apps (WSGI and ASGI). I looked in the documentation but could not find anything about this, and I could not get it to work, so I assume it is currently not supported by unit.

@tippexs tippexs added the z-python Language-Module for Python label Jan 31, 2024
@bunny-therapist
Copy link
Author

Flask supports factory thru the magic name "create_app" or "make_app": https://flask.palletsprojects.com/en/2.3.x/patterns/appfactories/

Personally, I would much prefer factories having zero arguments and then there is a "factory" boolean in the configuration, similar to uvicorn. (I find gunicorn's solution too obscure and flask's too complicated).

@callahad callahad added T-Enhancement New features and user-facing improvements L-Python labels Feb 8, 2024
@callahad
Copy link
Collaborator

callahad commented Feb 8, 2024

Thank you for filing this; we definitely want to support the application factory pattern. I especially appreciate your linking to gunicorn / uvicorn / Flask docs here.

We'll need to have a discussion to decide on the best pattern, but consider this officially on our backlog :)

@gourav-kandoria
Copy link
Contributor

gourav-kandoria commented Jun 17, 2024

@callahad Does this issue need work. would be happy to contribute to it.

I have a approach which I tested on my local. Not sure , This is 100% correct or not. Here it is.
Allow, to add a new parameter in the config like below:

{
    "applications": {
        "python-app": {
            "type": "python",
            "processes": 4,
            "path": "/www/",
            "targets": {
                "front": {
                    "module": "app",
                    "factory": "application_factory"
                },
                "back": {
                    "module": "app",
                    "factory": "application_factory"
                }
            }
        }
    },
    "listeners": {
        "127.0.0.1:8080": {
            "pass": "applications/python-app/front"
        }
    }
}

For, this need to make change in python related validation object like in
nxt_conf_vldt_python_notargets_members and nxt_conf_vldt_python_target_members. which will validate this field.

and now in the nxt_python_set_target, we can set target, if factory option is there.
by calling the application factory using PyObject_CallObject and setting returned object to the target.

If support for arguments for application factory need to be given, that can also be achieved with some additional changes

@ac000
Copy link
Member

ac000 commented Jun 17, 2024

I think what's missing is the why this is needed? and how does it relate to "app"? if you specify "factory" do you need to specify "app"?

@gourav-kandoria
Copy link
Contributor

gourav-kandoria commented Jun 17, 2024

If you are asking about why the separate "factory" option is needed. So my point is that "callable" by its name suggests that it is some callable which have interface as per wsgi or asgi standard. So, if "factory" as a different option is introduced that would mean it is some callable, if called would return callable supporting wsgi or asgi standard.

There can be other way of doing this like, if factory option is passed as true, then the callable should be considered as which factory which when called should return wsgi or asgi supporting callable

So, if we go by first way- then below is more explanation
It can be like either have callable option in the config or factory option . So both must be mutually exclusive. and in both cases , module have to be specified.

so , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "factory": "application_factory"
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

If we go by the second way then , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "callable": "application_factory",
                    "factory": true
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

@gourav-kandoria
Copy link
Contributor

If you are asking about why the separate "factory" option is needed. So my point is that "callable" by its name suggests that it is some callable which have interface as per wsgi or asgi standard. So, if "factory" as a different option is introduced that would mean it is some callable, if called would return callable supporting wsgi or asgi standard.

There can be other way of doing this like, if factory option is passed as true, then the callable should be considered as which factory which when called should return wsgi or asgi supporting callable

So, if we go by first way- then below is more explanation It can be like either have callable option in the config or factory option . So both must be mutually exclusive. and in both cases , module have to be specified.

so , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "factory": "application_factory"
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

If we go by the second way then , both the below given config can be said to correct

   "front": {
                    "module": "front",
                    "callable": "application_factory",
                    "factory": true
                },
                "front": {
                    "module": "front",
                    "callable": "app"
                },

any point or suggestion on it

@ac000
Copy link
Member

ac000 commented Jun 18, 2024

OK, thanks, (it didn't help I mixed callable and app up...). But, yes, you wouldn't specify callable & factory (unless you specify factory as true..)

So I take it "factory" is some Python terminology? I probably need to go read up on it sometime...

@bunny-therapist
Copy link
Author

"Factory" is just software terminology.

An "application factory" as input to a server runner is supported in a bunch of python server runners, as already described in the issue description. I am not sure about its prevalence in other languages since I don't have experience developing applications in those.

As for the discussion about format, I would personally prefer "callable" to be the path to both factory and app, and a "factory" boolean specifying if it is a factory or not. (The solution called "the second way" above.)

@ac000
Copy link
Member

ac000 commented Jun 18, 2024

Heh, not something I've come across in over 25 years in the world of C... ah, it's an OOP thing, well that explains everything...

@callahad
Copy link
Collaborator

If a Java design patterns textbook shows up on your doorstep, it wasn't me 🥸

@callahad
Copy link
Collaborator

@gourav-kandoria If you have a patch, would you mind opening a pull request? Even if it's unpolished, having something concrete is a great starting point.

If support for arguments for application factory need to be given, that can also be achieved with some additional changes

That's probably my biggest question at the moment. Honestly, my next step that I just haven't gotten around to yet is to survey how other WSGI/ASGI servers handle this pattern. If someone wants to do that research and write up a quick summary, I'd really appreciate it.

I guess I'd at least look at gunicorn, uWSGI, and mod_wsgi on the WSGI side, and uvicorn, hypercorn, and daphne on the ASGI side?

@callahad
Copy link
Collaborator

(Admittedly, I'm just cribbing from memory on the WSGI side and riffing off the Starlette docs for ASGI. If there are other popular servers not represented, do let me know)

@ac000
Copy link
Member

ac000 commented Jun 18, 2024

If a Java design patterns textbook shows up on your doorstep, it wasn't me 🥸

I could use a door stop!

@callahad
Copy link
Collaborator

callahad commented Jun 19, 2024

  • Gunicorn
    • Callable: $ gunicorn -w 4 'hello:app'
    • Factory: $ gunicorn -w 4 'hello:create_app()'
    • Positional and keyword arguments can also be passed, but it is recommended to load configuration from environment variables rather than the command line.

  • uWSGI
    • Callable: $ uwsgi --http 127.0.0.1:8000 --master -p 4 -w hello:app
    • Factory: Not supported (or is it?... the Flask docs suggest creating a shim).
    • Does not support passing arguments.
  • mod_wsgi
    • Uses WSGIScriptAlias to point to a shim that must define a callable named application
      • Can be renamed by setting WSGICallableObject
      • "It is not possible to use a dotted path to refer to a sub object of a module imported by the WSGI script file."

    • It is not possible to specify an application by Python module name alone

@callahad
Copy link
Collaborator

  • Uvicorn
    • Callable: $ uvicorn main:app
    • Factory: $ uvicorn --factory main:create_app
    • No support for passing args; specifically defines a factory as a "() -> <ASGI app> callable."
  • Hypercorn
    • Callable: $ hypercorn hello_world:app
    • Does not appear to support factories
  • Daphne
    • Callable: $ daphne django_project.asgi:application
    • Does not appear to support factories

@callahad
Copy link
Collaborator

So, it seems:

  1. Support for factories isn't ubiquitous (gunicorn, uvicorn, and maybe uwsgi?)
  2. Only gunicorn allows passing arguments to the factory

I find myself agreeing with @bunny-therapist's sensibilities:

I would much prefer factories having zero arguments and then there is a "factory" boolean in the configuration, similar to uvicorn. (I find gunicorn's solution too obscure and flask's too complicated).

"Explicit is better than implicit," right?

@callahad
Copy link
Collaborator

Concretely: Let's add an optional factory boolean that defaults to false but can be used like:

"hello": {
    "module": "hello",
    "callable": "create_app",
    "factory": true
}

To align with common usage, we may want to consider supporting module:app notation instead of separate module and callable keys, but that's out of scope for this discussion.

@ac000 ac000 linked a pull request Jun 20, 2024 that will close this issue
ac000 pushed a commit to gourav-kandoria/unit that referenced this issue Jun 20, 2024
This adds support for 'Application Factories' to the Python language
module.

This essentially allows you to run some code _before_ the main
application is loaded. This is similar to the '--factory' setting in
Uvicorn.

It can be configured like

  "python-app": {
      "type": "python",
      "path": "/srv/www",
      "module": "create_app",
      "factory": true
  }

The factory setting defaults to false.

Closes: nginx#1106
[ Commit subject, message and some minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
ac000 pushed a commit to gourav-kandoria/unit that referenced this issue Jun 20, 2024
This adds support for 'Application Factories' to the Python language
module.

This essentially allows you to run some code _before_ the main
application is loaded. This is similar to the '--factory' setting in
Uvicorn.

It can be configured like

  "python-app": {
      "type": "python",
      "path": "/srv/www",
      "module": "create_app",
      "factory": true
  }

The factory setting defaults to false.

Closes: nginx#1106
[ Commit subject, message and some minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
@callahad
Copy link
Collaborator

Amusingly, uWSGI does seem to support factories in the gunicorn style (appending () to the module name)

ac000 pushed a commit to gourav-kandoria/unit that referenced this issue Jun 21, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
ac000 pushed a commit to gourav-kandoria/unit that referenced this issue Jun 21, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 25, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>

python: Support application factories

Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 25, 2024
Adds support for the app factory pattern to the Python language module. A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the configured `callable` as a factory.

For example:

    "my-app": { "type": "python", "path": "/srv/www/", "module": "hello", "callable": "create_app", "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106 Link: <nginx#1336 (comment)> [ Commit message - Dan / Minor code
tweaks - Andrew ] Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 25, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 25, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable
4. When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable
4. When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable
4. When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable or When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jun 26, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable or When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
ac000 pushed a commit to gourav-kandoria/unit that referenced this issue Jun 28, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jul 1, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable or When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jul 1, 2024
Adds support for the app factory pattern to the Python language module.
A factory is a callable that returns a WSGI or ASGI application object.

Unit does not support passing arguments to factories.

Setting the `factory` option to `true` instructs Unit to treat the
configured `callable` as a factory.

For example:

    "my-app": {
        "type": "python",
        "path": "/srv/www/",
        "module": "hello",
        "callable": "create_app",
        "factory": true
    }

This is similar to other WSGI / ASGI servers. E.g.,

    $ uvicorn --factory hello:create_app
    $ gunicorn 'hello:create_app()'

The factory setting defaults to false.

Closes: nginx#1106
Link: <nginx#1336 (comment)>
[ Commit message - Dan / Minor code tweaks - Andrew ]
Signed-off-by: Andrew Clayton <[email protected]>
gourav-kandoria added a commit to gourav-kandoria/unit that referenced this issue Jul 1, 2024
…fig"

Add the following tests cases:
1. When "factory" key is used inside the "targets" option.
2. When "factory" key is used at the root level of python application config.
3. When factory returns invalid callable or When factory is invalid callable

Closes: nginx#1106
Link: <nginx#1336>
Signed-off-by: Andrew Clayton <[email protected]>
@ac000 ac000 closed this as completed in a9aa9e7 Jul 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
L-Python T-Enhancement New features and user-facing improvements z-python Language-Module for Python
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants