Skip to content

Commit

Permalink
#165: Apply pre-commit hooks to all files
Browse files Browse the repository at this point in the history
- Done by `pre-commit run --all-files`
  • Loading branch information
MRichards99 committed Oct 29, 2020
1 parent 348acf1 commit 7e7154c
Show file tree
Hide file tree
Showing 16 changed files with 64 additions and 60 deletions.
56 changes: 28 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,33 @@ ICAT API to interface with the Data Gateway
## Requirements
All requirements can be installed with `pip install -r requirements.txt`, and all development requirements can be installed with `pip install -r dev-requirements.txt`

The required python libraries:
- [SQLAlchemy](https://www.sqlalchemy.org/)
- [flask-restful](https://github.com/flask-restful/flask-restful/)
- [pymysql](https://pymysql.readthedocs.io/en/latest/)
The required python libraries:
- [SQLAlchemy](https://www.sqlalchemy.org/)
- [flask-restful](https://github.com/flask-restful/flask-restful/)
- [pymysql](https://pymysql.readthedocs.io/en/latest/)
- [pyyaml](https://pyyaml.org/wiki/PyYAMLDocumentation) (For the swagger generation)
- [pip-tools](https://github.com/jazzband/pip-tools) (For generating requirements.txt)

## Setup and running the API
## Setup and running the API
The database connection needs to be set up first. This is set in config.json, an example config file called `config.json.example` is provided.

Ideally the API would be run with:
Ideally the API would be run with:
`python -m src.main`
However it can be run with the flask run command as shown below:


**Warning: the host, port and debug config options will not be respected when the API is run this way**

To use `flask run`, the enviroment variable `FLASK_APP` should be set to `src/main.py`. Once this is
set the API can be run with `flask run` while inside the root directory of the project. The `flask run` command gets installed with flask.
To use `flask run`, the enviroment variable `FLASK_APP` should be set to `src/main.py`. Once this is
set the API can be run with `flask run` while inside the root directory of the project. The `flask run` command gets installed with flask.

Examples shown:
Examples shown:
Unix
```bash
$ export FLASK_APP=src/main.py
$ flask run
```
CMD
CMD
```CMD
> set FLASK_APP=src/main.py
> flask run
Expand Down Expand Up @@ -102,7 +102,7 @@ This is illustrated below.
│ ├── swagger
│ │ ├── openapi.yaml
│ │ └── swagger_generator.py
│ └── main.py
│ └── main.py
├── test
│ ├── resources
│ │ ├── entities
Expand All @@ -118,36 +118,36 @@ This is illustrated below.
└── config.json
`````
#### Main
`main.py` is where the flask_restful api is set up. This is where each endpoint resource class is generated and mapped
`main.py` is where the flask_restful api is set up. This is where each endpoint resource class is generated and mapped
to an endpoint.

Example:
`api.add_resource(get_endpoint(entity_name, endpoints[entity_name]), f"/{entity_name.lower()}")`
Example:
`api.add_resource(get_endpoint(entity_name, endpoints[entity_name]), f"/{entity_name.lower()}")`


#### Endpoints
The logic for each endpoint are within `/src/resources`. They are split into entities, non_entities and
The logic for each endpoint are within `/src/resources`. They are split into entities, non_entities and
table_endpoints. The entities package contains `entities_map` which maps entity names to their sqlalchemy
model. The `entity_endpoint` module contains the function that is used to generate endpoints at start up.
`table_endpoints` contains the endpoint classes that are table specific. Finally, non_entities contains the
session endpoint.


#### Mapped classes
The classes mapped from the database are stored in `/common/database/models.py`. Each model was
The classes mapped from the database are stored in `/common/database/models.py`. Each model was
automatically generated using sqlacodegen. A class `EntityHelper` is defined so that each model may
inherit two methods `to_dict()` and `update_from_dict(dictionary)`, both used for returning entities
and updating them, in a form easily converted to JSON.
inherit two methods `to_dict()` and `update_from_dict(dictionary)`, both used for returning entities
and updating them, in a form easily converted to JSON.




## Database Generator
There is a tool to generate mock data into the database. It is located in `util/icat_db_generator.py`
By default it will generate 20 years worth of data (approx 70,000 entities). The script makes use of
`random` and `Faker` and is seeded with a seed of 1. The seed and number of years of data generated can
By default it will generate 20 years worth of data (approx 70,000 entities). The script makes use of
`random` and `Faker` and is seeded with a seed of 1. The seed and number of years of data generated can
be changed by using the arg flags `-s` or `--seed` for the seed, and `-y` or `--years` for the number of years.
For example:
For example:
`python -m util.icat_db_generator -s 4 -y 10` Would set the seed to 4 and generate 10 years of data.


Expand All @@ -156,22 +156,22 @@ The querying and filtering logic is located in `/common/database_helpers.py`. In
`QueryFilter` classes are defined as well as their implementations. The functions that are used by various endpoints to
query the database are also in this module.
Class diagrams for this module:
![image](https://user-images.githubusercontent.com/44777678/67954353-ba69ef80-fbe8-11e9-81e3-0668cea3fa35.png)
![image](https://user-images.githubusercontent.com/44777678/67954353-ba69ef80-fbe8-11e9-81e3-0668cea3fa35.png)
![image](https://user-images.githubusercontent.com/44777678/67954834-7fb48700-fbe9-11e9-96f3-ffefc7277ebd.png)


#### Authentication
Each request requires a valid session ID to be provided in the Authorization header. This header should take the form of `{"Authorization":"Bearer <session_id>"}` A session ID can be obtained by
sending a post request to `/sessions/`
sending a post request to `/sessions/`
All endpoint methods that require a session id are decorated with `@requires_session_id`



#### Generating the swagger spec: `openapi.yaml`
The swagger generation script is located in `/src/swagger/swagger_generator.py`. The script will only run when
the config option `generate_swagger` is set to true in `config.json`. The generator decorates the first endpoint
resource class in it's module to get the name of the entity. It then creates the correct paths using the name of the
entity and outputs the swagger spec to `openapi.yaml`
resource class in it's module to get the name of the entity. It then creates the correct paths using the name of the
entity and outputs the swagger spec to `openapi.yaml`

Example of the decorator:
```python
Expand Down
4 changes: 0 additions & 4 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,3 @@
"host": "127.0.0.1",
"port": "5000"
}




2 changes: 1 addition & 1 deletion datagateway_api/common/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def get_investigations_for_instrument_in_facility_cycle_count_with_filters(
"""
Given an instrument id and facility cycle id, get the count of the
investigations that use the given instrument in the given cycle
:param session_id: The session id of the requesting user
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
Expand Down
4 changes: 2 additions & 2 deletions datagateway_api/common/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def get_entity_object_from_name(entity_name):
:param entity_name: Name of the entity to fetch a version from this model
:type entity_name: :class:`str`
:return: Object of the entity requested (e.g.
:return: Object of the entity requested (e.g.
:class:`common.database.models.INVESTIGATIONINSTRUMENT`)
:raises: KeyError: If an entity model cannot be found as a class in this model
"""
Expand Down Expand Up @@ -178,7 +178,7 @@ def update_from_dict(self, dictionary):
"""
Given a dictionary containing field names and variables, updates the entity from
the given dictionary
:param dictionary: dict: dictionary containing the new values
:returns: The updated dict
"""
Expand Down
2 changes: 1 addition & 1 deletion datagateway_api/common/filter_order_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def clear_python_icat_order_filters(self):
"""
Checks if any order filters have been added to the request and resets the
variable used to manage which attribute(s) to use for sorting results.
A reset is required because Python ICAT overwrites (as opposed to appending to
it) the query's order list every time one is added to the query.
"""
Expand Down
2 changes: 1 addition & 1 deletion datagateway_api/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def get_filters_from_query_string():
"""
Gets a list of filters from the query_strings arg,value pairs, and returns a list of
QueryFilter Objects
:return: The list of filters
"""
log.info(" Getting filters from query string")
Expand Down
13 changes: 8 additions & 5 deletions datagateway_api/common/icat/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
from datagateway_api.common.filter_order_handler import FilterOrderHandler
from datagateway_api.common.date_handler import DateHandler
from datagateway_api.common.constants import Constants
from datagateway_api.common.icat.filters import PythonICATLimitFilter, PythonICATWhereFilter
from datagateway_api.common.icat.filters import (
PythonICATLimitFilter,
PythonICATWhereFilter,
)
from datagateway_api.common.icat.query import ICATQuery


Expand All @@ -32,7 +35,7 @@ def requires_session_id(method):
Decorator for Python ICAT backend methods that looks out for session errors when
using the API. The API call runs and an ICATSessionError may be raised due to an
expired session, invalid session ID etc.
:param method: The method for the backend operation
:raises AuthenticationError: If a valid session_id is not provided with the request
"""
Expand Down Expand Up @@ -187,7 +190,7 @@ def get_entity_by_id(
:type entity_type: :class:`str`
:param id_: ID number of the entity to retrieve
:type id_: :class:`int`
:param return_json_formattable_data: Flag to determine whether the data should be
:param return_json_formattable_data: Flag to determine whether the data should be
returned as a list of data ready to be converted straight to JSON (i.e. if the
data will be used as a response for an API call) or whether to leave the data in
a Python ICAT format
Expand Down Expand Up @@ -362,7 +365,7 @@ def get_first_result_with_filters(client, entity_type, filters):

def update_entities(client, entity_type, data_to_update):
"""
Update one or more results for the given entity using the JSON provided in
Update one or more results for the given entity using the JSON provided in
`data_to_update`
:param client: ICAT client containing an authenticated user
Expand Down Expand Up @@ -549,7 +552,7 @@ def get_investigations_for_instrument_in_facility_cycle(
:param filters: The list of filters to be applied to the request
:type filters: List of specific implementations :class:`QueryFilter`
:param count_query: Flag to determine if the query in this function should be used
as a count query. Used for
as a count query. Used for
`get_investigations_for_instrument_in_facility_cycle_count()`
:type count_query: :class:`bool`
:return: A list of Investigations that match the query
Expand Down
12 changes: 6 additions & 6 deletions datagateway_api/common/icat/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(
self, client, entity_name, conditions=None, aggregate=None, includes=None,
):
"""
Create a Query object within Python ICAT
Create a Query object within Python ICAT
:param client: ICAT client containing an authenticated user
:type client: :class:`icat.client.Client`
Expand Down Expand Up @@ -60,7 +60,7 @@ def execute_query(self, client, return_json_formattable=False):
:param client: ICAT client containing an authenticated user
:type client: :class:`icat.client.Client`
:param return_json_formattable: Flag to determine whether the data from the
:param return_json_formattable: Flag to determine whether the data from the
query should be returned as a list of data ready to be converted straight to
JSON (i.e. if the data will be used as a response for an API call) or
whether to leave the data in a Python ICAT format (i.e. if it's going to be
Expand Down Expand Up @@ -164,7 +164,7 @@ def entity_to_dict(self, entity, includes, distinct_fields=None):
:class:`icat.entity.Entity`) or :class:`icat.entity.EntityList`
:param includes: List of fields that have been included in the ICAT query. It is
assumed each element has been checked for multiple fields separated by dots,
split them accordingly and flattened the resulting list. Note:
split them accordingly and flattened the resulting list. Note:
ICATQuery.flatten_query_included_fields performs this functionality.
:type includes: :class:`list`
:return: ICAT Data (of type dictionary) ready to be serialised to JSON
Expand Down Expand Up @@ -232,11 +232,11 @@ def map_distinct_attributes_to_entity_names(self, distinct_fields, included_fiel
entity they belong to
The result of this function will be a dictionary that has a data structure
similar to the example below. The values assigned to the 'base' key are the
similar to the example below. The values assigned to the 'base' key are the
fields that belong to the entity the request is being sent to (e.g. the base
values of `/users` would be fields belonging to the User entity).
Example return value:
Example return value:
`{'base': ['id', 'modTime'], 'userGroups': ['id', 'fullName'],
'investigationUser': ['id', 'role']}`
Expand Down Expand Up @@ -301,7 +301,7 @@ def prepare_distinct_fields_for_recursion(self, entity_name, distinct_fields):
Copy `distinct_fields` and move the data held in `entity_name` portion of the
dictionary to the "base" section of the dictionary. This function is called in
preparation for recursive calls occurring in entity_to_dict()
See map_distinct_attribute_to_entity_names() for an explanation regarding
`distinct_fields` and its data structure
Expand Down
2 changes: 1 addition & 1 deletion datagateway_api/src/resources/entities/entity_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def get_endpoint(name, entity_type):
Given an entity name generate a flask_restful Resource class.
In main.py these generated classes are registered with the api e.g
api.add_resource(get_endpoint("Datafiles", DATAFILE), "/datafiles")
:param name: The name of the entity
:param entity_type: The entity the endpoint will use in queries
:return: The generated endpoint class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def post(self):
example: xxxxxx-yyyyyyy-zzzzzz
400:
description: Bad request. User credentials were not provided in request body.
403:
403:
description: Forbidden. User credentials were invalid
"""
if not (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class InstrumentsFacilityCycles(Resource):
def get(self, id_):
"""
---
summary: Get an Instrument's FacilityCycles
summary: Get an Instrument's FacilityCycles
description: Given an Instrument id get facility cycles where the instrument has investigations that occur within that cycle, subject to the given filters
tags:
- FacilityCycles
Expand Down Expand Up @@ -64,7 +64,7 @@ class InstrumentsFacilityCyclesCount(Resource):
def get(self, id_):
"""
---
summary: Count an Instrument's FacilityCycles
summary: Count an Instrument's FacilityCycles
description: Return the count of the Facility Cycles that have investigations that occur within that cycle on the specified instrument that would be retrieved given the filters provided
tags:
- FacilityCycles
Expand Down Expand Up @@ -105,7 +105,7 @@ class InstrumentsFacilityCyclesInvestigations(Resource):
def get(self, instrument_id, cycle_id):
"""
---
summary: Get the investigations for a given Facility Cycle & Instrument
summary: Get the investigations for a given Facility Cycle & Instrument
description: Given an Instrument id and Facility Cycle id, get the investigations that occur within that cycle on that instrument, subject to the given filters
tags:
- Investigations
Expand Down Expand Up @@ -161,7 +161,7 @@ class InstrumentsFacilityCyclesInvestigationsCount(Resource):
def get(self, instrument_id, cycle_id):
"""
---
summary: Count investigations for a given Facility Cycle & Instrument
summary: Count investigations for a given Facility Cycle & Instrument
description: Given an Instrument id and Facility Cycle id, get the number of investigations that occur within that cycle on that instrument, subject to the given filters
tags:
- Investigations
Expand Down
2 changes: 1 addition & 1 deletion dev-requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ pip-tools == 5.3.1
Faker == 2.0.2
black == 19.10b0
nox==2020.8.22
flake8==3.8.4
flake8==3.8.4
4 changes: 2 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def lint(session):
"flake8-bugbear",
"flake8-import-order",
"flake8-builtins",
#"flake8-logging-format", # TODO - Add this to env
#"flake8-commas",
# "flake8-logging-format", # TODO - Add this to env
# "flake8-commas",
)
session.run("flake8", *args)

Expand Down
2 changes: 1 addition & 1 deletion postman_collection_icat.json
Original file line number Diff line number Diff line change
Expand Up @@ -29664,4 +29664,4 @@
}
],
"protocolProfileBehavior": {}
}
}
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ pymysql == 0.9.3
flask-cors == 3.0.8
apispec == 3.3.0
flask-swagger-ui == 3.25.0
pyyaml == 5.1.2
pyyaml == 5.1.2
7 changes: 6 additions & 1 deletion test/test_entityHelper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import datetime
from unittest import TestCase

from datagateway_api.common.database.models import DATAFILE, DATASET, DATAFILEFORMAT, INVESTIGATION
from datagateway_api.common.database.models import (
DATAFILE,
DATASET,
DATAFILEFORMAT,
INVESTIGATION,
)


class TestEntityHelper(TestCase):
Expand Down

0 comments on commit 7e7154c

Please sign in to comment.