Skip to content

Commit

Permalink
Unified API Browser when using modular server (#391)
Browse files Browse the repository at this point in the history
The expected behavior is that when using the modular way, the API
Browser merges in one, instead of having one API Browser for each.

Now, the server is aware of the `SERVER_NAME` Flask configuration,
it is being used by API Browser to request the correct server,
besides that, the API Browser is able to call servers in different
domains. For that configuration, the `JSONRPCSite` generates the
`path` and `base_url` variables from `SERVER_NAME`, `APPLICATION_ROOT`,
and `PREFERRED_URL_SCHEME`.

It is the first step to providing a Browse Schema to improve
documentation and examples from API (JSON-RPC methods).

Resolves: #388
See: #378, #377, #376, #374, #373, and #370
  • Loading branch information
nycholas authored Mar 25, 2023
1 parent 2c6afcd commit 13315b9
Show file tree
Hide file tree
Showing 23 changed files with 416 additions and 90 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/on_update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -57,6 +59,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand All @@ -80,6 +84,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/pre_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -53,6 +55,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -83,6 +87,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand All @@ -99,7 +105,7 @@ jobs:
bandit -r src/
- name: Check dependencies for known security vulnerabilities with Safety
run: |
safety check
safety check -i 52495 -i 51457
test:
name: Test
Expand All @@ -116,6 +122,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -153,6 +161,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -188,6 +198,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down Expand Up @@ -223,6 +235,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
steps:
- name: Checkout source at ${{ matrix.platform }}
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion bin/docker-compose-it.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DOCKER_COMPOSE_FILE_PATH=../${DOCKER_COMPOSE_FILE_NAME}
[ -f ${DOCKER_COMPOSE_FILE_PATH} ] || DOCKER_COMPOSE_FILE_PATH=${DOCKER_COMPOSE_FILE_NAME}

docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci_it build --build-arg VERSION=$(date +%s)
docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci_it up -d
docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci_it --compatibility up -d

DOCKER_WAIT_FOR_SUT=$(docker wait ci_it_sut_1)
docker logs ci_it_sut_1
Expand Down
2 changes: 1 addition & 1 deletion bin/docker-compose-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DOCKER_COMPOSE_FILE_PATH=../${DOCKER_COMPOSE_FILE_NAME}
[ -f ${DOCKER_COMPOSE_FILE_PATH} ] || DOCKER_COMPOSE_FILE_PATH=${DOCKER_COMPOSE_FILE_NAME}

docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci build --build-arg VERSION=$(date +%s)
docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci up -d
docker-compose -f ${DOCKER_COMPOSE_FILE_PATH} -p ci --compatibility up -d

DOCKER_WAIT_FOR_PY36=$(docker wait ci_python3.6_1)
docker logs ci_python3.6_1
Expand Down
6 changes: 4 additions & 2 deletions docker-compose.it.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
- SITE_PORT=5000
- WEB_URL=http://async_app:5000
- API_URL=http://async_app:5000/api
- BROWSABLE_API_URL=http://async_app:5000/browse
- BROWSABLE_API_URL=http://async_app:5000/api/browse
user: ${UID:-0}:${GID:-0}
depends_on:
- async_app
Expand All @@ -24,6 +24,7 @@ services:
- FLASK_ASYNC=1
environment:
- FLASK_ENV=TESTING
- FLASK_SERVER_NAME=async_app:5000
user: ${UID:-0}:${GID:-0}
command: >
python async_app.py
Expand All @@ -40,7 +41,7 @@ services:
- SITE_PORT=5000
- WEB_URL=http://app:5000
- API_URL=http://app:5000/api
- BROWSABLE_API_URL=http://app:5000/browse
- BROWSABLE_API_URL=http://app:5000/api/browse
user: ${UID:-0}:${GID:-0}
depends_on:
- app
Expand All @@ -51,6 +52,7 @@ services:
dockerfile: Dockerfile.local
environment:
- FLASK_ENV=TESTING
- FLASK_SERVER_NAME=app:5000
user: ${UID:-0}:${GID:-0}
command: >
python app.py
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ services:
pylint src/ tests/ &&
mypy --install-types --non-interactive src/ &&
bandit -r src/ &&
safety check &&
safety check -i 51457 &&
pytest"
python3.9:
Expand Down
File renamed without changes.
78 changes: 53 additions & 25 deletions src/flask_jsonrpc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import typing as t
from urllib.parse import urlsplit

from flask import Flask

from .globals import default_jsonrpc_site, default_jsonrpc_site_api
from .helpers import urn
from .wrappers import JSONRPCDecoratorMixin
from .contrib.browse import create_browse
from .contrib.browse import JSONRPCBrowse

if t.TYPE_CHECKING:
from .site import JSONRPCSite
Expand All @@ -49,10 +50,11 @@ def __init__(
enable_web_browsable_api: bool = False,
) -> None:
self.app = app
self.service_url = service_url
self.path = service_url
self.base_url: t.Optional[str] = None
self.jsonrpc_site = jsonrpc_site()
self.jsonrpc_site_api = jsonrpc_site_api
self.browse_url = self._make_browse_url(service_url)
self.jsonrpc_browse: t.Optional[JSONRPCBrowse] = None
self.enable_web_browsable_api = enable_web_browsable_api
if app:
self.init_app(app)
Expand All @@ -63,44 +65,70 @@ def get_jsonrpc_site(self) -> 'JSONRPCSite':
def get_jsonrpc_site_api(self) -> t.Type['JSONRPCView']:
return self.jsonrpc_site_api

def _make_browse_url(self, service_url: str) -> str:
return ''.join([service_url, '/browse']) if not service_url.endswith('/') else ''.join([service_url, 'browse'])
def _make_jsonrpc_browse_url(self, path: str) -> str:
return ''.join([path.rstrip('/'), '/browse'])

def init_app(self, app: Flask) -> None:
http_host = app.config.get('SERVER_NAME')
app_root = app.config['APPLICATION_ROOT']
url_scheme = app.config['PREFERRED_URL_SCHEME']
url = urlsplit(self.path)

self.path = f"{app_root.rstrip('/')}{url.path}"
self.base_url = (
f"{url.scheme or url_scheme}://{url.netloc or http_host}/{self.path.lstrip('/')}" if http_host else None
)

self.get_jsonrpc_site().set_path(self.path)
self.get_jsonrpc_site().set_base_url(self.base_url)

app.add_url_rule(
self.service_url,
self.path,
view_func=self.get_jsonrpc_site_api().as_view(
urn('app', app.name, self.service_url), jsonrpc_site=self.get_jsonrpc_site()
urn('app', app.name, self.path), jsonrpc_site=self.get_jsonrpc_site()
),
)
self.register_browse(app, self)

if app.config['DEBUG'] or self.enable_web_browsable_api:
self.init_browse_app(app)

def register(self, view_func: t.Callable[..., t.Any], name: t.Optional[str] = None, **options: t.Any) -> None:
self.register_view_function(view_func, name, **options)

def register_blueprint(
self, app: Flask, jsonrpc_app: 'JSONRPCBlueprint', url_prefix: str, enable_web_browsable_api: bool = False
self,
app: Flask,
jsonrpc_app: 'JSONRPCBlueprint',
url_prefix: t.Optional[str] = None,
enable_web_browsable_api: bool = False,
) -> None:
service_url = ''.join([self.service_url, url_prefix]) if url_prefix else self.service_url
path = ''.join([self.path, '/', url_prefix.lstrip('/')]) if url_prefix else self.path
path_url = urlsplit(path)

url = urlsplit(self.base_url or path)
base_url = f"{url.scheme}://{url.netloc}/{url.path.lstrip('/')}" if self.base_url else None

jsonrpc_app.get_jsonrpc_site().set_path(path_url.path)
jsonrpc_app.get_jsonrpc_site().set_base_url(base_url)

app.add_url_rule(
service_url,
path,
view_func=jsonrpc_app.get_jsonrpc_site_api().as_view(
urn('blueprint', app.name, jsonrpc_app.name, service_url), jsonrpc_site=jsonrpc_app.get_jsonrpc_site()
urn('blueprint', app.name, jsonrpc_app.name, path), jsonrpc_site=jsonrpc_app.get_jsonrpc_site()
),
)

if enable_web_browsable_api:
self.register_browse(app, jsonrpc_app, url_prefix=url_prefix)
if app.config['DEBUG'] or enable_web_browsable_api:
self.register_browse(jsonrpc_app)

def register_browse(
self, app: Flask, jsonrpc_app: t.Union['JSONRPC', 'JSONRPCBlueprint'], url_prefix: t.Optional[str] = None
) -> None:
browse_url = ''.join([self.service_url, url_prefix, '/browse']) if url_prefix else self.browse_url
if app.config['DEBUG'] or self.enable_web_browsable_api:
app.register_blueprint(
create_browse(urn('browse', app.name, browse_url), jsonrpc_app.get_jsonrpc_site()),
url_prefix=browse_url,
)
app.add_url_rule(
browse_url + '/static/<path:filename>', 'urn:browse.static', view_func=app.send_static_file
def init_browse_app(self, app: Flask, path: t.Optional[str] = None, base_url: t.Optional[str] = None) -> None:
browse_url = self._make_jsonrpc_browse_url(path or self.path)
self.jsonrpc_browse = JSONRPCBrowse(app, url_prefix=browse_url, base_url=base_url or self.base_url)
self.jsonrpc_browse.register_jsonrpc_site(self.get_jsonrpc_site())

def register_browse(self, jsonrpc_app: t.Union['JSONRPC', 'JSONRPCBlueprint']) -> None:
if not self.jsonrpc_browse:
raise RuntimeError(
'You need to init the Browse app before register the Site, see JSONRPC.init_browse_app(...)'
)
self.jsonrpc_browse.register_jsonrpc_site(jsonrpc_app.get_jsonrpc_site())
Loading

0 comments on commit 13315b9

Please sign in to comment.