GithubHelp home page GithubHelp logo

fps's Introduction

FPS

FPS (Fast Pluggable Server), is a framework designed to compose and run a web-server based on plugins. It is based on top of FastAPI, uvicorn, typer, and pluggy.

Motivation

To better understand the motivations behind this project, please refer to the Jupyter server team compass.

How it works

The main purpose of FPS is to provide hooks to register endpoints, static mounts, CLI setups/teardowns, etc.

An application can then be composed by multiple plugins providing specific/specialized endpoints. Those can be registered using fps.hooks.register_router with a fastapi.APIRouter.

What is coming soon

The most important parts will be to have a nice configuration system and also a logger working through multiprocesses, with homogeneous formatters to give devs/ops/users a smooth experience.

Concepts

Few concepts are extensively used in FPS:

  • a hook, or hook implementation, is a method tagged as implementing a hook specification
    • a hook specification is the declaration of the hook
      @pluggy.HookspecMarker(HookType.ROUTER.value)
      def router() -> APIRouter:
          pass
    • hooks are automatically collected by FPS using Python's entry_points, and ran at the right time
      [options.entry_points]
      fps_router =
          fps_helloworld_router = fps_helloworld.routes
      fps_config =
          fps_helloworld_config = fps_helloworld.config
    • multiple entry_points groups are defined (e.g. fps_router, fps_config, etc.)
      • a hook MUST be declared in its corresponding group to be collected
      • in the previous example, HookType.ROUTER.value equals fps_router, so the router hook is declared in that group
    • fps.hooks.register_<hook-name> helpers are returning such hooks
      def register_router(r: APIRouter):
          def router_callback() -> APIRouter:
              return r
      
          return pluggy.HookimplMarker(HookType.ROUTER.value)(
              function=router_callback, specname="router"
          )
  • a plugin is a Python module declared in a FPS's entry_point
    • a plugin may contain zero or more hooks
    • in the following helloworld example, the hook config is declared but not the plugin_name one. Both are hooks of the fps_config group.
      from fps.config import PluginModel
      from fps.hooks import register_config
      
      
      class HelloConfig(PluginModel):
          random: bool = True
      
      
      c = register_config(HelloConfig)
  • a plugins package is a Python package declaring one or more plugins

Configuration

FPS now support configuration using toml format.

Precedence order

For now, the loading sequence of the configuration is: fps.toml < <plugin-name>.toml < <cli-passed-file> < <cli-arg>.

fps.toml and <cli-passed-file> files can contain configuration of any plugin, while <plugin-name>.toml file will only be used for that specific plugin.

fps.toml and <plugin-name>.toml currently have to be in the current working directory. Support for loading from user home directory or system-wide application directory will be soon implemented.

Note: the environment variable FPS_CONFIG_FILE is used to store cli-passed filename and make it available to subprocesses.

Merging strategy

At this time the merging strategy between multiple config sources is pretty simple:

  • dict values for higher precedence source win
  • no appending/prepending on sequences

Testing

FPS has a testing module leveraging pytest fixtures and fastAPI dependencies override.

fps's People

Contributors

adriendelsalle avatar davidbrochart avatar github-actions[bot] avatar pre-commit-ci[bot] avatar willingc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

fps's Issues

open_browser: extra fields not permitted

Description

FPS master breaks jupyverse with:

[W 2021-10-15 15:38:23 uvicorn.error] Current configuration will not reload as not all conditions are met,please refer to documentation.
[I 2021-10-15 15:38:23 fps] Loading server configuration
Traceback (most recent call last):
  File "/home/david/mambaforge/envs/jupyverse/bin/jupyverse", line 8, in <module>
    sys.exit(app())
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/click/core.py", line 1128, in __call__
    return self.main(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/click/core.py", line 1053, in main
    rv = self.invoke(ctx)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/click/core.py", line 1395, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/click/core.py", line 754, in invoke
    return __callback(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/home/david/github/davidbrochart/fps/plugins/uvicorn/fps_uvicorn/cli.py", line 138, in start
    uvicorn.run(
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/uvicorn/main.py", line 447, in run
    server.run()
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/uvicorn/server.py", line 68, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/uvicorn/server.py", line 76, in serve
    config.load()
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/uvicorn/config.py", line 448, in load
    self.loaded_app = import_from_string(self.app)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
  File "/home/david/mambaforge/envs/jupyverse/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/home/david/github/davidbrochart/fps/fps/main.py", line 3, in <module>
    app = create_app()
  File "/home/david/github/davidbrochart/fps/fps/app.py", line 279, in create_app
    _load_configurations()
  File "/home/david/github/davidbrochart/fps/fps/app.py", line 86, in _load_configurations
    Config.register("fps", FPSConfig)
  File "/home/david/github/davidbrochart/fps/fps/config.py", line 107, in register
    config = config_model.parse_obj(config_obj)
  File "pydantic/main.py", line 578, in pydantic.main.BaseModel.parse_obj
  File "pydantic/main.py", line 406, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for FPSConfig
port
  extra fields not permitted (type=value_error.extra)

Reproduce

  1. Clone this repository.
  2. pip install -e .
  3. pip install -e plugins/uvicorn
  4. pip install jupyverse
  5. jupyverse --no-open-browser

App startup/shutdown hooks

It would be nice to be able to add hooks on app startup/shutdown, e.g. to connect/disconnect to/from a database:

@app.on_event("startup")
async def startup():
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

Make uvicorn optional

Problem

Currently, uvicorn is a dependency of FPS.

Proposed Solution

uvicorn should be an optional dependency, as FPS could be backed by any ASGI server.
Also, this would make it possible to use jupyverse in JupyterLite.

Run-time plugin router definition

Problem

Currently the API paths have to be defined at import-time, because FPS registers all the plugins through entry points and checks for path conflicts, etc. But we might want API paths to be configurable at run-time.

Proposed Solution

Could it be possible to defer the registration of routers, so that they have been configured before they are registered?
Maybe plugins could have initialize() and get_routers() callback hooks, where initialize passes configuration to a plugin before get_routers gets its routers back.

Additional context

For instance, Voila allows to configure the path to statically served directories, but if we want to implement that using FPS, we cannot use mounting points, we need to hack by defining an endpoint with a generic path, and implement the dynamic behavior inside the handler.

The default branch has been renamed!

master is now named main

If you have a local clone, you can update it by running the following commands.

git branch -m master main
git fetch origin
git branch -u origin/main main
git remote set-head origin -a

Report mount conflicts

Problem

Currently, endpoint path collision is detected by FPS, but there is a special case where it silently fails, which is when one plugin declares a mounting point at e.g. /static/lab and another plugin declares a mounting point at e.g. /static. In this case, the later will "overwrite" the former because it is at a higher level.

Proposed Solution

It would be great if FPS could report this kind of conflicts.

Inline listing of active endpoints in the console when starting the server

When using jupyverse to serve a Voilà dashboard instead of a Jupyter notebook, we should be careful about not activating FPS extensions that would give too much privileges

Proposal:

  • upon starting an instance of Jupyverse, list all active routes grouped by FPS extension in a rich fashion from the OpenAPI spec.

This may prevent some unintended exposures of insecure end points.

Eventually, a tool for displaying OpenAPI specs in the console could be a separate package (RichAPI?) and a useful utility beyond jupyverse.

cc @davidbrochart @bollwyvl @adriendelsalle

Expose FastAPI app or allow registering exception handlers

Problem

Working on jupyverse, I need to register an exception handler to be able to redirect from a dependency see tiangolo/fastapi#1039 (comment).
To register an exception handler we need access to the FastAPI app.

class RequiresLoginException(Exception):
    pass

@app.exception_handler(RequiresLoginException)
async def exception_handler(request: Request, exc: RequiresLoginException) -> Response:
    return RedirectResponse(url='/login')

Proposed Solution

Exposing the app, or allowing to register new exception handlers.

Additional context

I can try to open a PR but I need some help. I'm not sure what's the best way to proceed or how to implement it.

Improve enabled/disabled plugins config

Description

The first plugin given to --fps.disabled_plugins is not disabled.

Reproduce

  1. pip install jupyverse
  2. Launch jupyverse --fps.disabled_plugins=[authenticator,contents,kernels,Lab,nbconvert,terminals,yjs]
  3. Open a browser at http://127.0.0.1/docs
  4. The authenticator plugin's endpoints are visible.

Colors for HTTP error codes

It would be nice to have HTTP error codes not shown in green in the console, in order to see them more clearly.

Pass config parameters/file in pytest

A pytest fixture for a client of an FPS app looks like this:

import pytest
from fastapi.testclient import TestClient
from fps.main import app

@pytest.fixture()
def client():
    return TestClient(app)

It would be nice to be able to specify a config file when the app is used in pytest, and to mimic passing configuration options through the CLI with e.g. a dictionary.

Support file log handler

Problem

There seems to be no way to configure a logger with filehandler

I saw some code here https://github.com/jupyter-server/fps/blob/main/fps/logging.py#L189, and perhaps we should let the user configure handlers and give the optional handlers

Proposed Solution

support configure a logger with filehandler

Additional context

Once user-configurable filehandler is supported, perhaps also consider how to allow users to customize the location of log files

Don't silently accept non-existent options

Currently, passing configuration options that don't exist in a model is allowed and silent, e.g.:

jupyverse --auth.clear_db=true

where the real plugin name is authenticator or there is no clear_db in the config model.
It should error out or at least print a warning.

Error using pluggy 1.0

It looks like pluggy 1.0 breaks FPS:

  File "/home/david/github/davidbrochart/fps/fps/plugins.py", line 71, in load_configurations
    for plugin_model in pm._hookexec(pm.hook.config, get_hookimpls, {}):
TypeError: _hookexec() missing 1 required positional argument: 'firstresult'

Wrong routes / plugins

Description

Running jupyverse interactive docs shows:
image
You can see that the terminals endpoints are shown under the authenticator plugin.

Reproduce

  1. pip install jupyverse fastapi-users==7
  2. jupyverse --no-open-browser --authenticator.mode=noauth
  3. Open a browser to 127.0.0.1/docs
  4. See mixed routes between plugins

Detail path definition conflicts

Problem

Currently, when two or more plugins define an endpoint with the same path, we get an error message like:

[E 2021-10-06 15:41:06 fps] Redefinition of path(s) ['/lab/api/themes', '/'] is not allowed.

We don't know which plugins define the same endpoint.

Proposed Solution

It would be nice to have a more detailed error message, like:

[E 2021-10-06 15:41:06 fps] Redefinition of path(s) is not allowed:
[E 2021-10-06 15:41:06 fps] Path '/lab/api/themes' is defined by: JupyterLab, RetroLab.
[E 2021-10-06 15:41:06 fps] Path '/' is defined by: JupyterLab, RetroLab.

URL query parameters from plugins

Is it possible for a plugin to pass parameters back to FPS, so that these parameters appear as query parameters in the URL when the application is launched with --open-browser? The use-case I'm thinking of is when jupyverse is launched with token authentication: it should pass this token in the URL.

Forward CLI args to application

Problem

Currently, FPS "captures" CLI arguments and treats them only as configuration options. Anything else passed to the CLI fails, such as subcommands, or arguments and options that are not configuration-related.
For instance, in quetz run test_quetz --copy-conf ./dev_config.toml --dev --reload:

  • run is a subcommand.
  • test_quetz is a required argument.
  • --copy-conf is an option that has value ./dev_config.toml.
  • --dev and --reload are flags.

Proposed Solution

FPS should "forward" any subcommand, argument and options that are not configuration-related to the underlying application.

Additional context

This is needed if we want to use FPS in Quetz.

python3.7 error

It looks like FPS is not compatible with python3.7's logging module:

Traceback (most recent call last):
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/logging/config.py", line 543, in configure
    formatters[name])
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/logging/config.py", line 654, in configure_formatter
    result = self.configure_custom(config)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/logging/config.py", line 473, in configure_custom
    result = c(**kwargs)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/fps/logging.py", line 135, in colourized_formatter
    return ColourizedFormatter(fmt, use_colors=use_colors, datefmt=datefmt)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/fps/logging.py", line 72, in __init__
    super().__init__(fmt=fmt, datefmt=datefmt, style="coloured%", validate=True)
TypeError: __init__() got an unexpected keyword argument 'validate'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/david/mambaforge/envs/jupyverse-dev/bin/jupyverse", line 33, in <module>
    sys.exit(load_entry_point('jupyverse', 'console_scripts', 'jupyverse')())
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/click/core.py", line 1137, in __call__
    return self.main(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/click/core.py", line 1062, in main
    rv = self.invoke(ctx)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/click/core.py", line 763, in invoke
    return __callback(*args, **kwargs)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/typer/main.py", line 500, in wrapper
    return callback(**use_params)  # type: ignore
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/fps/cli.py", line 115, in start
    log_config=configure_logger(("uvicorn", "uvicorn.access", "uvicorn.error")),
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/site-packages/fps/logging.py", line 234, in configure_logger
    logging.config.dictConfig(log_config)
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/logging/config.py", line 800, in dictConfig
    dictConfigClass(config).configure()
  File "/home/david/mambaforge/envs/jupyverse-dev/lib/python3.7/logging/config.py", line 546, in configure
    'formatter %r' % name) from e
ValueError: Unable to configure formatter 'colour'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.