Skip to content

Commit

Permalink
Merge pull request #2 from evo-company/add-aiohttp-and-requests-client
Browse files Browse the repository at this point in the history
Add aiohttp and requests client
  • Loading branch information
n4mespace authored Jan 19, 2024
2 parents 62e60cf + f1113d2 commit e4c3514
Show file tree
Hide file tree
Showing 38 changed files with 768 additions and 414 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ README.md
.lets
helm
.ipython
.ptpython
.secrets

# Ignore IDE settings
Expand Down
31 changes: 17 additions & 14 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
name: Test

# TODO: enable after test fixes
on: workflow_dispatch
# pull_request:
# branches:
# - main
# types:
# - assigned
# - opened
# - synchronize
# - reopened
on:
pull_request:
branches:
- main
types:
- assigned
- opened
- synchronize
- reopened

jobs:
test-client:
test-clients:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12]
python-version: [3.8, 3.9, "3.10", 3.11, 3.12]
steps:
- uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: pdm-project/setup-pdm@v3
uses: pdm-project/setup-pdm@v3.3
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: python -m pip install tox tox-gh-actions tox-pdm

- name: Test with tox
run: |
tox --version
Expand Down
27 changes: 27 additions & 0 deletions .hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash

HAS_STAGED_PY=$(git diff --staged --diff-filter=d --name-only '*.py')

if [ -n "$HAS_STAGED_PY" ]; then

echo "Running mypy ..."
lets mypy
if [[ $? -ne 0 ]]; then
exit 1
fi

echo "Running black ..."
lets black --diff --check
if [[ $? -ne 0 ]]; then
exit 1
fi

echo "Running ruff ..."
lets ruff-diff
if [[ $? -ne 0 ]]; then
exit 1
fi

fi

exit 0
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ RUN pdm install --no-lock -G dev -G lint --no-editable
FROM dev as examples
RUN pdm install --no-lock -G examples

FROM base as test
FROM dev as test
RUN pdm install --no-lock -G test

FROM base as docs
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ Development

Install dependencies:
- ``pdm install -d``

Pre-commit

``./scripts/enable-hooks.sh``

``./scripts/disable-hooks.sh``


TODO:
- add docs, automate docs build
- add tests
- add `tracer` / `stats_collector` for http manager
- rm old grpc client
- add publish workflow
10 changes: 8 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ services:
LC_ALL: C.UTF-8
PYTHONIOENCODING: UTF-8
PYTHONUNBUFFERED: 1
networks:
- main
volumes:
- ./featureflags_client:/app/featureflags_client
- ./examples:/app/examples
- ./.ipython:/app/.ipython
- ./.ptpython:/app/.ptpython
# Uncomment to mount local build of `featureflags_protobuf`
- ./featureflags_protobuf:/app/featureflags_protobuf

ishell:
<<: *base
image: featureflags-client-examples
command: pdm run ipython --ipython-dir=/app/.ipython
command: pdm run ishell

test:
<<: *base
Expand All @@ -30,3 +32,7 @@ services:
<<: *base
image: featureflags-client-docs
command: sphinx-build -a -b html docs public

networks:
main:
driver: bridge
55 changes: 11 additions & 44 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,32 @@
Examples
========

TODO: refactor examples

Here you can find examples for gRPC and HTTP clients with:

- `AIOHTTP`
- `aiohttp`
- `Sanic`
- `Flask`
- `WSGI`
- `httpx`

Prerequisites:

.. code-block:: shell

$ pip install featureflags-client

If you're using AsyncIO:

.. code-block:: shell

$ pip install grpclib

else:

.. code-block:: shell

$ pip install grpcio

Configuration for all examples located in ``config.py`` module.

Feature flags and variables are defined in ``flags.py`` module.

Every example starts a HTTP server and available on http://localhost:5000

AIOHTTP:

.. code-block:: shell

$ PYTHONPATH=../client:../protobuf python aiohttp_app.py

Sanic:
- sync + grpc:

.. code-block:: shell
> pip install featureflags-client[grpclib]
$ PYTHONPATH=../client:../protobuf python sanic_app.py
- async + grpc:

Flask:
> pip install featureflags-client[grpcio]
.. code-block:: shell
- async + http:

$ PYTHONPATH=../client:../protobuf python flask_app.py
> pip install featureflags-client[httpx]
WSGI:
or

.. code-block:: shell
> pip install featureflags-client[aiohttp]
$ PYTHONPATH=../client:../protobuf python wsgi_app.py
- sync + http:

.. _AIOHTTP: https://aiohttp.readthedocs.io/
.. _Sanic: https://sanic.readthedocs.io/
.. _Flask: http://flask.pocoo.org
.. _WSGI: https://www.python.org/dev/peps/pep-0333/
> pip install featureflags-client[requests]
70 changes: 70 additions & 0 deletions examples/http/aiohttp_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging

import config
import flags
from aiohttp import web

from featureflags_client.http.client import FeatureFlagsClient
from featureflags_client.http.managers.aiohttp import AiohttpManager

log = logging.getLogger(__name__)


async def on_start(app):
app["ff_manager"] = AiohttpManager(
url=config.FF_URL,
project=config.FF_PROJECT,
variables=[flags.REQUEST_QUERY],
defaults=flags.Defaults,
)
app["ff_client"] = FeatureFlagsClient(app["ff_manager"])

try:
await app["ff_client"].preload_async(timeout=5)
except Exception:
log.exception(
"Unable to preload feature flags, application will "
"start working with defaults and retry later"
)

# Async managers need to `start` and `wait_closed` to be able to
# run flags update loop
app["ff_manager"].start()


async def on_stop(app):
await app["ff_manager"].wait_closed()


@web.middleware
async def middleware(request, handler):
ctx = {flags.REQUEST_QUERY.name: request.query_string}
with request.app["ff_client"].flags(ctx) as ff:
request["ff"] = ff
return await handler(request)


async def index(request):
if request["ff"].TEST:
return web.Response(text="TEST: True")
else:
return web.Response(text="TEST: False")


def create_app():
app = web.Application(middlewares=[middleware])

app.router.add_get("/", index)
app.on_startup.append(on_start)
app.on_cleanup.append(on_stop)

app["config"] = config

return app


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.getLogger("featureflags").setLevel(logging.DEBUG)

web.run_app(create_app(), port=5000)
4 changes: 2 additions & 2 deletions examples/http/httpx_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from aiohttp import web

from featureflags_client.http.client import FeatureFlagsClient
from featureflags_client.http.managers.httpx import AsyncHttpManager
from featureflags_client.http.managers.httpx import HttpxManager

log = logging.getLogger(__name__)


async def on_start(app):
app["ff_manager"] = AsyncHttpManager(
app["ff_manager"] = HttpxManager(
url=config.FF_URL,
project=config.FF_PROJECT,
variables=[flags.REQUEST_QUERY],
Expand Down
61 changes: 61 additions & 0 deletions examples/http/requests_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import logging

import config
import flags
from flask import Flask, g, request
from werkzeug.local import LocalProxy

from featureflags_client.http.client import FeatureFlagsClient
from featureflags_client.http.managers.requests import RequestsManager

app = Flask(__name__)


def get_ff_client():
ff_client = getattr(g, "_ff_client", None)
if ff_client is None:
manager = RequestsManager(
url=config.FF_URL,
project=config.FF_PROJECT,
variables=[flags.REQUEST_QUERY],
defaults=flags.Defaults,
)
ff_client = g._ff_client = FeatureFlagsClient(manager)
return ff_client


def get_ff():
if "_ff" not in g:
g._ff_ctx = get_ff_client().flags(
{
flags.REQUEST_QUERY.name: request.query_string,
}
)
g._ff = g._ff_ctx.__enter__()
return g._ff


@app.teardown_request
def teardown_request(exception=None):
if "_ff" in g:
g._ff_ctx.__exit__(None, None, None)
del g._ff_ctx
del g._ff


ff = LocalProxy(get_ff)


@app.route("/")
def index():
if ff.TEST:
return "TEST: True"
else:
return "TEST: False"


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
logging.getLogger("featureflags").setLevel(logging.DEBUG)

app.run(port=5000)
Loading

0 comments on commit e4c3514

Please sign in to comment.