Skip to content
This repository was archived by the owner on Oct 23, 2023. It is now read-only.

Commit 9f1adeb

Browse files
authored
Merge pull request #148 from CSCfi/feature/type-hints
Feature/type hints
2 parents b662ba8 + 84405ff commit 9f1adeb

25 files changed

Lines changed: 295 additions & 196 deletions

.github/workflows/publish.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,45 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@v1
1515
- name: Login to DockerHub Registry
16-
run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
16+
run: echo '${{ secrets.DOCKER_PASSWORD }}' | docker login -u '${{ secrets.DOCKER_USERNAME }}' --password-stdin
1717
- name: Get the version
1818
id: vars
1919
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10})
2020
- name: Build the tagged Docker image
21+
if: ${{ steps.vars.outputs.tag != '/master' }}
2122
run: docker build . --file Dockerfile --tag cscfi/beacon-python:${{steps.vars.outputs.tag}}
2223
- name: Push the tagged Docker image
24+
if: ${{ steps.vars.outputs.tag != '/master' }}
2325
run: docker push cscfi/beacon-python:${{steps.vars.outputs.tag}}
2426
- name: Build the latest Docker image
27+
if: ${{ steps.vars.outputs.tag == '/master' }}
2528
run: docker build . --file Dockerfile --tag cscfi/beacon-python:latest
2629
- name: Push the latest Docker image
30+
if: ${{ steps.vars.outputs.tag == '/master' }}
2731
run: docker push cscfi/beacon-python:latest
2832
push_data_to_registry:
2933
name: Push Dataloader Docker image to Docker Hub
3034
runs-on: ubuntu-latest
3135
steps:
3236
- uses: actions/checkout@v1
3337
- name: Login to DockerHub Registry
34-
run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
38+
run: echo '${{ secrets.DOCKER_PASSWORD }}' | docker login -u '${{ secrets.DOCKER_USERNAME }}' --password-stdin
3539
- name: Get the version
3640
id: vars
3741
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:10})
3842
- name: Build the tagged Docker image
43+
if: ${{ steps.vars.outputs.tag != '/master' }}
3944
run: |
4045
pushd deploy/test
4146
docker build . --file Dockerfile --tag cscfi/beacon-dataloader:${{steps.vars.outputs.tag}}
4247
- name: Push the tagged Docker image
48+
if: ${{ steps.vars.outputs.tag != '/master' }}
4349
run: docker push cscfi/beacon-dataloader:${{steps.vars.outputs.tag}}
4450
- name: Build the latest Docker image
51+
if: ${{ steps.vars.outputs.tag == '/master' }}
4552
run: |
4653
pushd deploy/test
4754
docker build . --file Dockerfile --tag cscfi/beacon-dataloader:latest
4855
- name: Push the latest Docker image
49-
run: docker push cscfi/beacon-dataloader:latest
56+
if: ${{ steps.vars.outputs.tag == '/master' }}
57+
run: docker push cscfi/beacon-dataloader:latest

.github/workflows/style.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ jobs:
2525
- name: Test flake8 syntax with tox
2626
run: tox -e flake8
2727
- name: Do bandit static check with tox
28-
run: tox -e bandit
28+
run: tox -e bandit
29+
- name: Install libcurl-devel
30+
run: sudo apt-get install libcurl4-openssl-dev
31+
- name: Do typing check with tox
32+
run: tox -e mypy

beacon_api/api/exceptions.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
"""
55

66
import json
7+
from typing import Dict
78
from aiohttp import web
89
from .. import __apiVersion__
910
from ..utils.logging import LOG
1011
from ..conf import CONFIG_INFO
1112

1213

13-
def process_exception_data(request, host, error_code, error):
14+
def process_exception_data(request: Dict,
15+
host: str,
16+
error_code: int,
17+
error: str) -> Dict:
1418
"""Return request data as dictionary.
1519
1620
Generates custom exception messages based on request parameters.
@@ -45,7 +49,8 @@ class BeaconBadRequest(web.HTTPBadRequest):
4549
Used in conjunction with JSON Schema validator.
4650
"""
4751

48-
def __init__(self, request, host, error):
52+
def __init__(self, request: Dict,
53+
host: str, error: str) -> None:
4954
"""Return custom bad request exception."""
5055
data = process_exception_data(request, host, 400, error)
5156
super().__init__(text=json.dumps(data), content_type="application/json")
@@ -59,7 +64,8 @@ class BeaconUnauthorised(web.HTTPUnauthorized):
5964
Used in conjunction with Token authentication aiohttp middleware.
6065
"""
6166

62-
def __init__(self, request, host, error, error_message):
67+
def __init__(self, request: Dict,
68+
host: str, error: str, error_message: str) -> None:
6369
"""Return custom unauthorized exception."""
6470
data = process_exception_data(request, host, 401, error)
6571
headers_401 = {"WWW-Authenticate": f"Bearer realm=\"{CONFIG_INFO.url}\"\n\
@@ -79,7 +85,8 @@ class BeaconForbidden(web.HTTPForbidden):
7985
but not granted the resource. Used in conjunction with Token authentication aiohttp middleware.
8086
"""
8187

82-
def __init__(self, request, host, error):
88+
def __init__(self, request: Dict,
89+
host: str, error: str) -> None:
8390
"""Return custom forbidden exception."""
8491
data = process_exception_data(request, host, 403, error)
8592
super().__init__(content_type="application/json", text=json.dumps(data))
@@ -92,7 +99,7 @@ class BeaconServerError(web.HTTPInternalServerError):
9299
The 500 error is not specified by the Beacon API, thus as simple error would do.
93100
"""
94101

95-
def __init__(self, error):
102+
def __init__(self, error: str) -> None:
96103
"""Return custom forbidden exception."""
97104
data = {'errorCode': 500,
98105
'errorMessage': error}

beacon_api/api/info.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
.. note:: See ``beacon_api`` root folder ``__init__.py`` for changing values used here.
77
"""
88

9+
from typing import Dict
910
from .. import __apiVersion__, __title__, __version__, __description__, __url__, __alturl__, __handover_beacon__
1011
from .. import __createtime__, __updatetime__, __org_id__, __org_name__, __org_description__
1112
from .. import __org_address__, __org_logoUrl__, __org_welcomeUrl__, __org_info__, __org_contactUrl__
@@ -17,7 +18,7 @@
1718

1819

1920
@cached(ttl=60, key="ga4gh_info", serializer=JsonSerializer())
20-
async def ga4gh_info(host):
21+
async def ga4gh_info(host: str) -> Dict:
2122
"""Construct the `Beacon` app information dict in GA4GH Discovery format.
2223
2324
:return beacon_info: A dict that contain information about the ``Beacon`` endpoint.
@@ -43,7 +44,7 @@ async def ga4gh_info(host):
4344

4445

4546
@cached(ttl=60, key="info_key", serializer=JsonSerializer())
46-
async def beacon_info(host, pool):
47+
async def beacon_info(host: str, pool) -> Dict:
4748
"""Construct the `Beacon` app information dict.
4849
4950
:return beacon_info: A dict that contain information about the ``Beacon`` endpoint.

beacon_api/api/query.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
start or end position.
66
"""
77

8+
from typing import Dict, Tuple, List, Optional
89
from ..utils.logging import LOG
910
from .. import __apiVersion__, __handover_beacon__, __handover_drs__
1011
from ..utils.data_query import filter_exists, find_datasets, fetch_datasets_access
@@ -13,7 +14,11 @@
1314
from .exceptions import BeaconUnauthorised, BeaconForbidden, BeaconBadRequest
1415

1516

16-
def access_resolution(request, token, host, public_data, registered_data, controlled_data):
17+
def access_resolution(request: Dict, token: Dict,
18+
host: str,
19+
public_data: List[str],
20+
registered_data: List[str],
21+
controlled_data: List[str]) -> Tuple[List[str], List[str]]:
1722
"""Determine the access level for a user.
1823
1924
Depends on user bona_fide_status, and by default it should be PUBLIC.
@@ -23,12 +28,12 @@ def access_resolution(request, token, host, public_data, registered_data, contro
2328
# unless the request is for specific datasets
2429
if public_data:
2530
permissions.append("PUBLIC")
26-
access = set(public_data) # empty if no datasets are given
31+
accessible_datasets = set(public_data) # empty if no datasets are given
2732

2833
# for now we are expecting that the permissions are a list of datasets
2934
if registered_data and token["bona_fide_status"] is True:
3035
permissions.append("REGISTERED")
31-
access = access.union(set(registered_data))
36+
accessible_datasets = accessible_datasets.union(set(registered_data))
3237
# if user requests public datasets do not throw an error
3338
# if both registered and controlled datasets are request this will be shown first
3439
elif registered_data and not public_data:
@@ -43,7 +48,7 @@ def access_resolution(request, token, host, public_data, registered_data, contro
4348
# Default event, when user doesn't specify dataset ids
4449
# Contains only dataset ids from token that are present at beacon
4550
controlled_access = set(controlled_data).intersection(set(token['permissions']))
46-
access = access.union(controlled_access)
51+
accessible_datasets = accessible_datasets.union(controlled_access)
4752
if controlled_access:
4853
permissions.append("CONTROLLED")
4954
# if user requests public datasets do not throw an error
@@ -54,11 +59,12 @@ def access_resolution(request, token, host, public_data, registered_data, contro
5459
raise BeaconUnauthorised(request, host, "missing_token", 'Unauthorized access to dataset(s), missing token.')
5560
# token is present, but is missing perms (user authed but no access)
5661
raise BeaconForbidden(request, host, 'Access to dataset(s) is forbidden.')
57-
LOG.info(f"Accesible datasets are: {list(access)}.")
58-
return permissions, list(access)
5962

63+
LOG.info(f"Accesible datasets are: {list(accessible_datasets)}.")
64+
return permissions, list(accessible_datasets)
6065

61-
async def query_request_handler(params):
66+
67+
async def query_request_handler(params: Tuple) -> Dict:
6268
"""Handle the parameters of the query endpoint in order to find the required datasets.
6369
6470
params = db_pool, method, request, token, host
@@ -91,18 +97,20 @@ async def query_request_handler(params):
9197
raise BeaconBadRequest(request, params[4], "endMin value Must be smaller than endMax value")
9298
if request.get("startMin") and request.get("startMin") > request.get("startMax"):
9399
raise BeaconBadRequest(request, params[4], "startMin value Must be smaller than startMax value")
94-
requested_position = (request.get("start", None), request.get("end", None),
95-
request.get("startMin", None), request.get("startMax", None),
96-
request.get("endMin", None), request.get("endMax", None))
100+
requested_position: Tuple[Optional[int], ...] = (request.get("start", None), request.get("end", None),
101+
request.get("startMin", None), request.get("startMax", None),
102+
request.get("endMin", None), request.get("endMax", None))
97103

98104
# Get dataset ids that were requested, sort by access level
99105
# If request is empty (default case) the three dataset variables contain all datasets by access level
100106
# Datasets are further filtered using permissions from token
101107
public_datasets, registered_datasets, controlled_datasets = await fetch_datasets_access(params[0], request.get("datasetIds"))
102-
access_type, accessible_datasets = access_resolution(request, params[3], params[4], public_datasets,
103-
registered_datasets, controlled_datasets)
108+
access_type, accessible_datasets = access_resolution(request,
109+
params[3], params[4],
110+
public_datasets, registered_datasets, controlled_datasets)
104111
if 'mateName' in request or alleleRequest.get('variantType') == 'BND':
105-
datasets = await find_fusion(params[0], request.get("assemblyId"), requested_position, request.get("referenceName"),
112+
datasets = await find_fusion(params[0],
113+
request.get("assemblyId"), requested_position, request.get("referenceName"),
106114
request.get("referenceBases"), request.get('mateName'),
107115
accessible_datasets, access_type, request.get("includeDatasetResponses", "NONE"))
108116
else:
@@ -122,4 +130,5 @@ async def query_request_handler(params):
122130

123131
if __handover_drs__:
124132
beacon_response['beaconHandover'] = make_handover(__handover_beacon__, [x['datasetId'] for x in datasets])
133+
125134
return beacon_response

beacon_api/app.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
# ----------------------------------------------------------------------------------------------------------------------
2929
@routes.get('/') # For Beacon API Specification
3030
@routes.get('/service-info') # For GA4GH Discovery Specification
31-
async def beacon_get(request):
31+
async def beacon_get(request: web.Request) -> web.Response:
3232
"""
3333
Use the HTTP protocol 'GET' to return a Json object of all the necessary info on the beacon and the API.
3434
@@ -54,7 +54,7 @@ async def beacon_get(request):
5454
# These could be put under a @route.view('/query')
5555
@routes.get('/query')
5656
@validate(load_schema("query"))
57-
async def beacon_get_query(request):
57+
async def beacon_get_query(request: web.Request) -> web.Response:
5858
"""Find datasets using GET endpoint."""
5959
method, processed_request = await parse_request_object(request)
6060
params = request.app['pool'], method, processed_request, request["token"], request.host
@@ -64,15 +64,15 @@ async def beacon_get_query(request):
6464

6565
@routes.post('/query')
6666
@validate(load_schema("query"))
67-
async def beacon_post_query(request):
67+
async def beacon_post_query(request: web.Request) -> web.Response:
6868
"""Find datasets using POST endpoint."""
6969
method, processed_request = await parse_request_object(request)
7070
params = request.app['pool'], method, processed_request, request["token"], request.host
7171
response = await query_request_handler(params)
7272
return web.json_response(response, content_type='application/json', dumps=json.dumps)
7373

7474

75-
async def initialize(app):
75+
async def initialize(app: web.Application) -> None:
7676
"""Spin up DB a connection pool with the HTTP server."""
7777
# TO DO check if table and Database exist
7878
# and maybe exit gracefully or at least wait for a bit
@@ -81,7 +81,7 @@ async def initialize(app):
8181
set_cors(app)
8282

8383

84-
async def destroy(app):
84+
async def destroy(app: web.Application) -> None:
8585
"""Upon server close, close the DB connection pool."""
8686
# will defer this to asyncpg
8787
await app['pool'].close() # pragma: no cover
@@ -104,7 +104,7 @@ def set_cors(server):
104104
cors.add(route)
105105

106106

107-
async def init():
107+
async def init() -> web.Application:
108108
"""Initialise server."""
109109
beacon = web.Application(middlewares=[token_auth()])
110110
beacon.router.add_routes(routes)
@@ -129,6 +129,6 @@ def main():
129129

130130
if __name__ == '__main__':
131131
if sys.version_info < (3, 6):
132-
LOG.error("beacon-python requires python3.6")
132+
LOG.error("beacon-python requires python 3.6")
133133
sys.exit(1)
134134
main()

beacon_api/conf/__init__.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
"""Beacon Python Application Configuration."""
22

33
import json
4-
import os
4+
from os import environ
55
from configparser import ConfigParser
66
from collections import namedtuple
77
from distutils.util import strtobool
8+
from pathlib import Path
89

10+
from typing import Any, Dict, List, Union
911

10-
def parse_drspaths(paths):
12+
13+
def convert(dictionary: Dict) -> tuple:
14+
"""Convert dictionary to Named tuple."""
15+
return namedtuple('Config', dictionary.keys())(**dictionary)
16+
17+
18+
def parse_drspaths(paths: str) -> List[List[str]]:
1119
"""Parse handover configuration."""
1220
return [p.strip().split(',', 2) for p in paths.split('\n') if p.split()]
1321

1422

15-
def parse_config_file(path):
23+
def parse_config_file(path) -> Any:
1624
"""Parse configuration file."""
1725
config = ConfigParser()
1826
config.read(path)
19-
config_vars = {
27+
config_vars: Dict[str, Union[str, int, List[List[str]]]] = {
2028
'title': config.get('beacon_general_info', 'title'),
2129
'version': config.get('beacon_general_info', 'version'),
2230
'author': config.get('beacon_general_info', 'author'),
@@ -45,28 +53,28 @@ def parse_config_file(path):
4553
'org_logoUrl': config.get('organisation_info', 'org_logoUrl'),
4654
'org_info': config.get('organisation_info', 'org_info')
4755
}
48-
return namedtuple("Config", config_vars.keys())(*config_vars.values())
56+
return convert(config_vars)
4957

5058

51-
CONFIG_INFO = parse_config_file(os.environ.get('CONFIG_FILE', os.path.join(os.path.dirname(__file__), 'config.ini')))
59+
CONFIG_INFO = parse_config_file(environ.get('CONFIG_FILE', str(Path(__file__).resolve().parent.joinpath('config.ini'))))
5260

5361

54-
def parse_oauth2_config_file(path):
62+
def parse_oauth2_config_file(path: str) -> Any:
5563
"""Parse configuration file."""
5664
config = ConfigParser()
5765
config.read(path)
58-
config_vars = {
66+
config_vars: Dict[str, Union[str, bool, None]] = {
5967
'server': config.get('oauth2', 'server'),
6068
'issuers': config.get('oauth2', 'issuers'),
6169
'userinfo': config.get('oauth2', 'userinfo'),
6270
'audience': config.get('oauth2', 'audience') or None,
6371
'verify_aud': bool(strtobool(config.get('oauth2', 'verify_aud'))),
6472
'bona_fide_value': config.get('oauth2', 'bona_fide_value')
6573
}
66-
return namedtuple("Config", config_vars.keys())(*config_vars.values())
74+
return convert(config_vars)
6775

6876

69-
OAUTH2_CONFIG = parse_oauth2_config_file(os.environ.get('CONFIG_FILE', os.path.join(os.path.dirname(__file__), 'config.ini')))
77+
OAUTH2_CONFIG = parse_oauth2_config_file(environ.get('CONFIG_FILE', str(Path(__file__).resolve().parent.joinpath('config.ini'))))
7078
# Sample query file should be of format [{BeaconAlleleRequest}] https://github.com/ga4gh-beacon/specification/
71-
sampleq_file = os.environ.get('SAMPLEQUERY_FILE', os.path.join(os.path.dirname(__file__), 'sample_queries.json'))
72-
SAMPLE_QUERIES = json.load(open(sampleq_file)) if os.path.isfile(sampleq_file) else []
79+
sampleq_file = Path(environ.get('SAMPLEQUERY_FILE', str(Path(__file__).resolve().parent.joinpath('sample_queries.json'))))
80+
SAMPLE_QUERIES = json.load(open(sampleq_file)) if sampleq_file.is_file() else []

beacon_api/conf/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616

1717
import os
1818
import asyncpg
19+
from typing import Awaitable
1920

2021
DB_SCHEMA = os.environ.get('DATABASE_SCHEMA', None)
2122

2223

23-
async def init_db_pool():
24+
async def init_db_pool() -> Awaitable:
2425
"""Create a connection pool.
2526
2627
As we will have frequent requests to the database it is recommended to create a connection pool.

0 commit comments

Comments
 (0)