Skip to content

Commit

Permalink
Settings plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
madkote committed Mar 31, 2021
1 parent 7cf3769 commit cb31f5e
Show file tree
Hide file tree
Showing 13 changed files with 843 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Changes
## 0.8.0 (2020-03-31)
- `[feature]` Settings plugin
## 0.7.0 (2020-03-29)
- `[feature]` Control plugin with Health, Heartbeat, Environment and Version
## 0.6.1 (2020-03-24)
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
</p>

# fastapi-plugins
FastAPI framework plugins
FastAPI framework plugins - simple way to share `fastapi` code and utilities across applications.

The concept is `plugin` - plug a functional utility into your application without or with minimal effort.

* [Cache](./docs/cache.md)
* [Memcached](./docs/cache.md#memcached)
Expand All @@ -28,6 +30,7 @@ FastAPI framework plugins
* [Environment](./docs/control.md#environment)
* [Health](./docs/control.md#health)
* [Heartbeat](./docs/control.md#heartbeat)
* [Application settings/configuration](./docs/settings.md)
* Celery
* MQ
* and much more is already in progress...
Expand All @@ -50,6 +53,13 @@ pip install fastapi-plugins[all]
```

## Quick start
### Plugin
Add information about plugin system.
### Application settings
Add information about settings.
### Application configuration
Add information about configuration of an application
### Complete example
```python
import fastapi
import fastapi_plugins
Expand All @@ -63,6 +73,7 @@ import asyncio
import aiojobs
import aioredis

@fastapi_plugins.registered_configuration
class AppSettings(
fastapi_plugins.ControlSettings,
fastapi_plugins.RedisSettings,
Expand All @@ -71,14 +82,22 @@ class AppSettings(
):
api_name: str = str(__name__)


@fastapi_plugins.registered_configuration(name='sentinel')
class AppSettingsSentinel(AppSettings):
redis_type = fastapi_plugins.RedisType.sentinel
redis_sentinels = 'localhost:26379'


app = fastapi.FastAPI()
config = AppSettings()
config = fastapi_plugins.get_config()

@app.get("/")
async def root_get(
cache: aioredis.Redis=fastapi.Depends(fastapi_plugins.depends_redis),
conf: pydantic.BaseSettings=fastapi.Depends(fastapi_plugins.depends_config) # noqa E501
) -> typing.Dict:
return dict(ping=await cache.ping())
return dict(ping=await cache.ping(), api_name=conf.api_name)


@app.post("/jobs/schedule/<timeout>")
Expand Down Expand Up @@ -132,6 +151,8 @@ async def memcached_demo_post(

@app.on_event('startup')
async def on_startup() -> None:
await fastapi_plugins.config_plugin.init_app(app, config)
await fastapi_plugins.config_plugin.init()
await memcached_plugin.init_app(app, config)
await memcached_plugin.init()
await fastapi_plugins.redis_plugin.init_app(app, config=config)
Expand All @@ -153,6 +174,7 @@ async def on_shutdown() -> None:
await fastapi_plugins.scheduler_plugin.terminate()
await fastapi_plugins.redis_plugin.terminate()
await memcached_plugin.terminate()
await fastapi_plugins.config_plugin.terminate()
```

# Development
Expand Down
163 changes: 163 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Settings
The easy way to configure the `FastAPI` application:
* define (various) configuration(s)
* register configuration
* use configuration depending on the environment
* use configuration in router handler if required

## Define configuration
It is a good practice to defined various configuration for your application.
```python
import fastapi_plugins

class DefaultSettings(fastapi_plugins.RedisSettings):
api_name: str = str(__name__)

class DockerSettings(DefaultSettings):
redis_type = fastapi_plugins.RedisType.sentinel
redis_sentinels = 'localhost:26379'

class LocalSettings(DefaultSettings):
pass

class TestSettings(DefaultSettings):
testing: bool = True

class MyConfigSettings(DefaultSettings):
custom: bool = True

...
```

## Register configuration
Registration with a decorator
```python
import fastapi_plugins

class DefaultSettings(fastapi_plugins.RedisSettings):
api_name: str = str(__name__)

# @fastapi_plugins.registered_configuration_docker
@fastapi_plugins.registered_configuration
class DockerSettings(DefaultSettings):
redis_type = fastapi_plugins.RedisType.sentinel
redis_sentinels = 'localhost:26379'

@fastapi_plugins.registered_configuration_local
class LocalSettings(DefaultSettings):
pass

@fastapi_plugins.registered_configuration_test
class TestSettings(DefaultSettings):
testing: bool = True

@fastapi_plugins.registered_configuration(name='my_config')
class MyConfigSettings(DefaultSettings):
custom: bool = True
...
```

or by a function call
```python
import fastapi_plugins

class DefaultSettings(fastapi_plugins.RedisSettings):
api_name: str = str(__name__)

class DockerSettings(DefaultSettings):
redis_type = fastapi_plugins.RedisType.sentinel
redis_sentinels = 'localhost:26379'

class LocalSettings(DefaultSettings):
pass

class TestSettings(DefaultSettings):
testing: bool = True

class MyConfigSettings(DefaultSettings):
custom: bool = True

fastapi_plugins.register_config(DockerSettings)
# fastapi_plugins.register_config_docker(DockerSettings)
fastapi_plugins.register_config_local(LocalSettings)
fastapi_plugins.register_config_test(TestSettings)
fastapi_plugins.register_config(MyConfigSettings, 'my_config')
...
```


## Application configuration
Next, create application and it's configuration, and register the plugin if needed. The last is optinally,
and is only relevant for use cases, where the configuration values are required for endpoint handlers (see `api_name` below).
```python
import fastapi
import fastapi_plugins
...

app = fastapi.FastAPI()
config = fastapi_plugins.get_config()

@app.get("/")
async def root_get(
cache: aioredis.Redis=fastapi.Depends(fastapi_plugins.depends_redis),
conf: pydantic.BaseSettings=fastapi.Depends(fastapi_plugins.depends_config) # noqa E501
) -> typing.Dict:
return dict(ping=await cache.ping(), api_name=conf.api_name)

@app.on_event('startup')
async def on_startup() -> None:
await fastapi_plugins.config_plugin.init_app(app, config)
await fastapi_plugins.config_plugin.init()
await fastapi_plugins.redis_plugin.init_app(app, config=config)
await fastapi_plugins.redis_plugin.init()
await fastapi_plugins.control_plugin.init_app(app, config=config, version=__version__, environ=config.dict())
await fastapi_plugins.control_plugin.init()


@app.on_event('shutdown')
async def on_shutdown() -> None:
await fastapi_plugins.control_plugin.terminate()
await fastapi_plugins.redis_plugin.terminate()
await fastapi_plugins.config_plugin.terminate()
```

## Use configuration
Now, the application will use by default a configuration for `docker` or by user's command any other configuration.
```bash
uvicorn scripts.demo_app:app
curl -X 'GET' 'http://localhost:8000/control/environ' -H 'accept: application/json'
{
"environ": {
"config_name":"docker",
"redis_type":"sentinel",
...
}
}

...
CONFIG_NAME=local uvicorn scripts.demo_app:app
curl -X 'GET' 'http://localhost:8000/control/environ' -H 'accept: application/json'
{
"environ": {
"config_name":"local",
"redis_type":"redis",
...
}
}
```

It is also usefull with `docker-compose`:
```yaml
services:
demo_fastapi_plugin:
image: demo_fastapi_plugin
environment:
- CONFIG_NAME=docker

...

demo_fastapi_plugin:
image: demo_fastapi_plugin
environment:
- CONFIG_NAME=docker_sentinel
```
5 changes: 1 addition & 4 deletions fastapi_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .control import * # noqa F401 F403
from ._redis import * # noqa F401 F403
from .scheduler import * # noqa F401 F403
from .settings import * # noqa F401 F403
from .version import VERSION

# try:
Expand All @@ -41,8 +42,6 @@
# TODO: provide a generic cache type (redis, memcached, in-memory)
# and share some settings. Module/Sub-Pack cache

# TODO: health

# TODO: databases

# TODO: mq - activemq, rabbitmq, kafka
Expand All @@ -51,8 +50,6 @@

# TODO: celery

# TOOD: abstract routers with configurable endpoints

# TODO: check socketio (python-socketio) - do we need this?

# TODO: look at fastapi-cache (memcache?) look at mqtt?
Expand Down
6 changes: 4 additions & 2 deletions fastapi_plugins/_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,7 @@ async def health(self) -> typing.Dict:
redis_plugin = RedisPlugin()


async def depends_redis(request: starlette.requests.Request) -> aioredis.Redis:
return await request.app.state.REDIS()
async def depends_redis(
conn: starlette.requests.HTTPConnection
) -> aioredis.Redis:
return await conn.app.state.REDIS()
15 changes: 12 additions & 3 deletions fastapi_plugins/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ class ControlHeartBeat(ControlBaseModel):
)


# class ControlInfo(ControlBaseModel):
# status: str = pydantic.Field(
# 'API is up and running',
# title='status',
# min_length=1,
# exampe='API is up and running'
# )


class ControlVersion(ControlBaseModel):
version: str = pydantic.Field(
...,
Expand Down Expand Up @@ -322,7 +331,7 @@ async def init_app(
raise ControlError('Control configuration is not initialized')
elif not isinstance(self.config, self.DEFAULT_CONFIG_CLASS):
raise ControlError('Control configuration is not valid')
app.state.CONTROL = self
app.state.PLUGIN_CONTROL = self
#
# initialize here while `app` is available
self.controller = Controller(
Expand Down Expand Up @@ -361,6 +370,6 @@ async def terminate(self):


async def depends_control(
request: starlette.requests.Request
conn: starlette.requests.HTTPConnection
) -> Controller:
return await request.app.state.CONTROL()
return await conn.app.state.PLUGIN_CONTROL()
4 changes: 2 additions & 2 deletions fastapi_plugins/memcached.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,6 @@ async def health(self) -> typing.Dict:


async def depends_memcached(
request: starlette.requests.Request
conn: starlette.requests.HTTPConnection
) -> MemcachedClient:
return await request.app.state.MEMCACHED()
return await conn.app.state.MEMCACHED()
4 changes: 2 additions & 2 deletions fastapi_plugins/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,6 @@ async def health(self) -> typing.Dict:


async def depends_scheduler(
request: starlette.requests.Request
conn: starlette.requests.HTTPConnection
) -> aiojobs.Scheduler:
return await request.app.state.AIOJOBS_SCHEDULER()
return await conn.app.state.AIOJOBS_SCHEDULER()
Loading

0 comments on commit cb31f5e

Please sign in to comment.