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

Start testing and linting #14

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Python testing

on:
- push
- pull_request

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

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

- name: Test with tox
run: tox
2 changes: 2 additions & 0 deletions ai4papi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.

"""AI4EOSC Platform API."""

from . import version

__version__ = version.release_string
80 changes: 38 additions & 42 deletions ai4papi/auth.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
"""
Authentication for private methods of the API (mainly managing deployments)

Implementation notes:
====================
Authentication is implemented using `get_user_infos_from_access_token` instead
of `get_user_infos_from_request` (as done in the FastAPI example in the flaat docs).
There are two advantages of this:
* the main one is that it would enable us enable to take advantage of Swagger's builtin
Authentication and Authorization [1] in the Swagger interface (generated by FastAPI).
This is not possible using the `Request` object, as data from `Request` cannot be validated and
documented by OpenAPI [2].

[1] https://swagger.io/docs/specification/authentication/
[2] https://fastapi.tiangolo.com/advanced/using-request-directly/?h=request#details-about-the-request-object

* the decorator `flaat.is_authenticated()` around each function is no longer needed,
as authentication is checked automatically by `authorization=Depends(security)` without needing extra code.

The curl calls still remain the same, but now in the http://localhost/docs you will see an authorize
button where you can copy paste your token. So you will be able to access authenticated methods from the interface.
"""
"""Authentication for private methods of the API (mainly managing deployments)."""

# Implementation notes:
# Authentication is implemented using `get_user_infos_from_access_token` instead
# of `get_user_infos_from_request` (as done in the FastAPI example in the flaat docs).
# There are two advantages of this:
# * the main one is that it would enable us enable to take advantage of Swagger's
# builtin Authentication and Authorization [1] in the Swagger interface (generated by
# FastAPI). This is not possible using the `Request` object, as data from `Request`
# cannot be validated and documented by OpenAPI [2].
#
# [1] https://swagger.io/docs/specification/authentication/
# [2] https://fastapi.tiangolo.com/advanced/using-request-directly
#
# * the decorator `flaat.is_authenticated()` around each function is no longer needed,
# as authentication is checked automatically by `authorization=Depends(security)`
# without needing extra code.
#
# The curl calls still remain the same, but now in the http://localhost/docs you will
# see an authorize button where you can copy paste your token. So you will be able to
# access authenticated methods from the interface.

import re

Expand All @@ -35,56 +34,53 @@


def get_user_info(token):

"""Return user information from a token."""
try:
user_infos = flaat.get_user_infos_from_access_token(token)
except Exception as e:
raise HTTPException(
status_code=403,
detail=str(e),
)
)

# Check output
if user_infos is None:
raise HTTPException(
status_code=403,
detail="Invalid token",
)
)

# Check scopes
if user_infos.get('eduperson_entitlement') is None:
if user_infos.get("eduperson_entitlement") is None:
raise HTTPException(
status_code=403,
detail="Check you enabled the `eduperson_entitlement` scope for your token.",
)
detail="Check you enabled the `eduperson_entitlement` scope for the token.",
)

# Parse Virtual Organizations manually from URNs
# If more complexity is need in the future, check https://github.com/oarepo/urnparse
vos = []
for i in user_infos.get('eduperson_entitlement'):
vos.append(
re.search(r"group:(.+?):", i).group(1)
)
for i in user_infos.get("eduperson_entitlement"):
vos.append(re.search(r"group:(.+?):", i).group(1))

# Filter VOs to keep only the ones relevant to us
vos = set(vos).intersection(
set(MAIN_CONF['auth']['VO'])
)
vos = set(vos).intersection(set(MAIN_CONF["auth"]["VO"]))
vos = sorted(vos)

# Check if VOs is empty after filtering
if not vos:
raise HTTPException(
status_code=403,
detail=f"You should belong to at least one of the Virtual Organizations supported by the project: {vos}.",
)
detail="You should belong to at least one of the Virtual Organizations "
f"supported by the project: {vos}.",
)

# Generate user info dict
out = {
'id': user_infos.get('sub'), # subject, user-ID
'issuer': user_infos.get('iss'), # URL of the access token issuer
'name': user_infos.get('name'),
'vo': vos,
"id": user_infos.get("sub"), # subject, user-ID
"issuer": user_infos.get("iss"), # URL of the access token issuer
"name": user_infos.get("name"),
"vo": vos,
}

return out
14 changes: 6 additions & 8 deletions ai4papi/cli.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
"""
Command Line Interface to the API
"""Command Line Interface to the API."""

Implementation notes:
====================
Typer is not implement directly as a decorator around the run() function in main.py
because it gave errors while trying to run `uvicorn main:app --reload`.
Just have it in a separate file, clean and easy.
"""
# Implementation notes:
# Typer is not implement directly as a decorator around the run() function in main.py
# because it gave errors while trying to run `uvicorn main:app --reload`. Just have it
# in a separate file, clean and easy.

import typer

from ai4papi import main


def run():
"""Run the API."""
typer.run(main.run)


Expand Down
21 changes: 10 additions & 11 deletions ai4papi/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
Manage configurations of the API.
"""
"""Manage configurations of the API."""

from pathlib import Path

Expand All @@ -10,27 +8,28 @@

Nomad = nomad.Nomad()

CONF_DIR = Path(__file__).parents[1].absolute() / 'etc'
CONF_DIR = Path(__file__).parents[1].absolute() / "etc"

# Load main API configuration
with open(CONF_DIR / 'main_conf.yaml', 'r') as f:
with open(CONF_DIR / "main_conf.yaml", "r") as f:
MAIN_CONF = yaml.safe_load(f)

# Load default Nomad job configuration
with open(CONF_DIR / 'job.nomad', 'r') as f:
with open(CONF_DIR / "job.nomad", "r") as f:
job_raw_nomad = f.read()
NOMAD_JOB_CONF = Nomad.jobs.parse(job_raw_nomad)

# Load user customizable parameters
with open(CONF_DIR / 'userconf.yaml', 'r') as f:
with open(CONF_DIR / "userconf.yaml", "r") as f:
USER_CONF = yaml.safe_load(f)

USER_CONF_VALUES = {}
for group_name, params in USER_CONF.items():
USER_CONF_VALUES[group_name] = {}
for k, v in params.items():
assert 'name' in v.keys(), f"Parameter {k} needs to have a name."
assert 'value' in v.keys(), f"Parameter {k} needs to have a value."

USER_CONF_VALUES[group_name][k] = v['value']
if "name" not in v.keys():
raise KeyError(f"Parameter {k} needs to have a name in `userconf.yaml`.")
if "value" not in v.keys():
raise KeyError(f"Parameter {k} needs to have a value in `userconf.yaml`.")

USER_CONF_VALUES[group_name][k] = v["value"]
20 changes: 12 additions & 8 deletions ai4papi/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""
Create an app with FastAPI
"""
"""AI4EOSC Plaform API (AI4PAPI) with FastAPI."""

import ipaddress
import typing

import fastapi
from fastapi.security import HTTPBearer
import typer
import uvicorn

from ai4papi.routers import v1
Expand Down Expand Up @@ -38,6 +40,7 @@
def root(
request: fastapi.Request,
):
"""Get version and documentation endpoints."""
root = str(request.url_for("root"))
versions = [v1.get_version(request)]

Expand Down Expand Up @@ -65,11 +68,12 @@ def root(


def run(
host:str = "0.0.0.0",
port:int = 8080,
ssl_keyfile:str = None,
ssl_certfile:str = None,
):
host: ipaddress.IPv4Address = ipaddress.IPv4Address("127.0.0.1"), # noqa(B008)
port: int = 8080,
ssl_keyfile: typing.Optional[pathlib.Path] = typer.Option(None), # noqa(B008)
ssl_certfile: typing.Optional[pathlib.Path] = typer.Option(None), # noqa(B008)
):
"""Run the API in uvicorn."""
uvicorn.run(
app,
host=host,
Expand Down
1 change: 1 addition & 0 deletions ai4papi/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Route definition and management."""
2 changes: 2 additions & 0 deletions ai4papi/routers/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""V1 routes."""

import fastapi

Expand All @@ -16,6 +17,7 @@
tags=["API", "version"],
)
def get_version(request: fastapi.Request):
"""Get V1 version information."""
root = str(request.url_for("get_version"))
# root = "/"
version = {
Expand Down
Loading