Skip to content
Snippets Groups Projects
Commit aea9280a authored by Bjarke Madsen's avatar Bjarke Madsen
Browse files

Merge branch 'feature/COMP-114_python_code_quality' into 'develop'

Feature/comp 114 python code quality

See merge request !12
parents 91780b31 df9908ae
No related branches found
No related tags found
1 merge request!12Feature/comp 114 python code quality
Showing
with 179 additions and 231 deletions
......@@ -17,6 +17,22 @@ def migrate_database(config: dict) -> None:
migration_utils.upgrade(dsn)
def _create_app(app_config) -> Flask:
# used by sphinx to create documentation without config and db migrations
app = Flask(__name__)
CORS(app)
app.config['CONFIG_PARAMS'] = app_config
from compendium_v2.routes import default
app.register_blueprint(default.routes, url_prefix='/')
from compendium_v2.routes import api
app.register_blueprint(api.routes, url_prefix='/api')
return app
def create_app() -> Flask:
"""
overrides default settings with those found
......@@ -25,22 +41,12 @@ def create_app() -> Flask:
:return: a new flask app instance
"""
assert 'SETTINGS_FILENAME' in os.environ, \
"environment variable 'SETTINGS_FILENAME' is required"
assert 'SETTINGS_FILENAME' in os.environ, "environment variable 'SETTINGS_FILENAME' is required"
with open(os.environ['SETTINGS_FILENAME']) as f:
app_config = config.load(f)
app = Flask(__name__)
CORS(app)
app.config['CONFIG_PARAMS'] = app_config
from compendium_v2.routes import default
app.register_blueprint(default.routes, url_prefix='/')
from compendium_v2.routes import api
app.register_blueprint(api.routes, url_prefix='/api')
app = _create_app(app_config)
logging.info('Flask app initialized')
......
import logging
import openpyxl
import os
import logging
from compendium_v2.db.model import FeeType
from compendium_v2.environment import setup_logging
......@@ -9,9 +9,7 @@ setup_logging()
logger = logging.getLogger(__name__)
EXCEL_FILE = os.path.join(
os.path.dirname(__file__), "xlsx",
"2021_Organisation_DataSeries.xlsx")
EXCEL_FILE = os.path.join(os.path.dirname(__file__), "xlsx", "2021_Organisation_DataSeries.xlsx")
def fetch_budget_excel_data():
......@@ -32,12 +30,7 @@ def fetch_budget_excel_data():
if budget is not None:
budget = round(budget / 1000000, 2)
if budget > 200:
logger.info(
f'{nren} has budget set to '
f'>200M EUR for {year}. ({budget})')
# process the data (e.g. save to database)
# print(f"NREN: {nren}, Budget: {budget}, Year: {year}")
logger.info(f'{nren} has budget set to >200M EUR for {year}. ({budget})')
yield nren.upper(), budget, year
......@@ -52,17 +45,12 @@ def fetch_funding_excel_data():
def hard_number_convert(s, source_name, nren, year):
if s is None:
logger.info(
f'Invalid Value :{nren} has empty value for {source_name}.'
+ f'for year ({year})')
logger.info(f'Invalid Value :{nren} has empty value for {source_name} for year ({year})')
return float(0)
""" Returns True if string is a number. """
try:
return float(s)
except ValueError:
logger.info(
f'Invalid Value :{nren} has empty value for {source_name}.'
+ f'for year ({year}) with value ({s})')
logger.info(f'Invalid Value :{nren} has empty value for {source_name} for year ({year}) with value ({s})')
return float(0)
# iterate over the rows in the worksheet
......@@ -76,7 +64,6 @@ def fetch_funding_excel_data():
gov_public_bodies = ws.cell(row=row, column=col_start + 3).value
other_european_funding = ws.cell(row=row, column=col_start + 4).value
other = ws.cell(row=row, column=col_start + 5).value
print(nren, client_institution, commercial, geant_subsidy, gov_public_bodies, other_european_funding, other)
client_institution = hard_number_convert(client_institution, "client institution", nren, year)
commercial = hard_number_convert(commercial, "commercial", nren, year)
......@@ -89,10 +76,7 @@ def fetch_funding_excel_data():
# process the data (e.g. save to database)
if nren is not None:
yield (nren.upper(), year, client_institution,
european_funding,
gov_public_bodies,
commercial, other)
yield (nren.upper(), year, client_institution, european_funding, gov_public_bodies, commercial, other)
def create_points_for_year_from_2018(start_row, end_row, year, col_start):
for row in range(start_row, end_row):
......@@ -112,10 +96,7 @@ def fetch_funding_excel_data():
# process the data (e.g. save to database)
if nren is not None:
yield (nren.upper(), year, client_institution,
european_funding,
gov_public_bodies,
commercial, other)
yield (nren.upper(), year, client_institution, european_funding, gov_public_bodies, commercial, other)
# For 2016
yield from create_points_for_year_until_2017(8, 50, 2016, 43, 45)
......@@ -147,26 +128,22 @@ def fetch_charging_structure_excel_data():
# extract the data from the row
nren = ws.cell(row=row, column=col_start).value
charging_structure = ws.cell(row=row, column=col_start + 1).value
logger.info(
f'NREN: {nren}, Charging Structure: {charging_structure},'
f' Year: {year}')
logger.info(f'NREN: {nren}, Charging Structure: {charging_structure}, Year: {year}')
if charging_structure is not None:
if "do not charge" in charging_structure:
charging_structure = FeeType.no_charge.value
charging_structure = FeeType.no_charge
elif "combination" in charging_structure:
charging_structure = FeeType.combination.value
charging_structure = FeeType.combination
elif "flat" in charging_structure:
charging_structure = FeeType.flat_fee.value
charging_structure = FeeType.flat_fee
elif "usage-based" in charging_structure:
charging_structure = FeeType.usage_based_fee.value
charging_structure = FeeType.usage_based_fee
elif "Other" in charging_structure:
charging_structure = FeeType.other.value
charging_structure = FeeType.other
else:
charging_structure = None
logger.info(
f'NREN: {nren}, Charging Structure: {charging_structure},'
f' Year: {year}')
logger.info(f'NREN: {nren}, Charging Structure: {charging_structure}, Year: {year}')
yield nren.upper(), year, charging_structure
......
import json
import jsonschema
CONFIG_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'definitions': {
'database-uri': {
'type': 'string'
}
},
'properties': {
'SQLALCHEMY_DATABASE_URI': {
'type': 'string',
'properties': {
'database-uri': {'$ref': '#definitions/database-uri'}
},
'additionalProperties': False
},
'SURVEY_DATABASE_URI': {
'type': 'string',
'properties': {
'database-uri': {'$ref': '#definitions/database-uri'}
},
'additionalProperties': False
}
'SQLALCHEMY_DATABASE_URI': {'type': 'string', 'format': 'database-uri'},
'SURVEY_DATABASE_URI': {'type': 'string', 'format': 'database-uri'}
},
'required': ['SQLALCHEMY_DATABASE_URI', 'SURVEY_DATABASE_URI'],
'additionalProperties': False
......
import logging
import sqlalchemy as sa
from decimal import Decimal
from enum import Enum
from typing import Optional
from typing_extensions import Annotated
from typing import Any
from sqlalchemy import MetaData, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.schema import ForeignKey
from sqlalchemy import MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
logger = logging.getLogger(__name__)
......@@ -20,37 +21,42 @@ convention = {
metadata_obj = MetaData(naming_convention=convention)
# https://github.com/python/mypy/issues/2477
base_schema: Any = declarative_base(metadata=metadata_obj)
str128 = Annotated[str, 128]
int_pk = Annotated[int, mapped_column(primary_key=True)]
int_pk_fkNREN = Annotated[int, mapped_column(ForeignKey("nren.id"), primary_key=True)]
class NREN(base_schema):
class Base(DeclarativeBase):
metadata = metadata_obj
type_annotation_map = {
str128: String(128),
}
class NREN(Base):
__tablename__ = 'nren'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(128), nullable=False)
id: Mapped[int_pk]
name: Mapped[str128]
class BudgetEntry(base_schema):
class BudgetEntry(Base):
__tablename__ = 'budgets'
nren_id = sa.Column(
sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
budget = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
budget: Mapped[Decimal]
class FundingSource(base_schema):
class FundingSource(Base):
__tablename__ = 'funding_source'
nren_id = sa.Column(
sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
client_institutions = sa.Column(
sa.Numeric(asdecimal=False), nullable=False)
european_funding = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
gov_public_bodies = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
commercial = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
other = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
client_institutions: Mapped[Decimal]
european_funding: Mapped[Decimal]
gov_public_bodies: Mapped[Decimal]
commercial: Mapped[Decimal]
other: Mapped[Decimal]
class FeeType(Enum):
......@@ -61,49 +67,45 @@ class FeeType(Enum):
other = "other"
class ChargingStructure(base_schema):
class ChargingStructure(Base):
__tablename__ = 'charging_structure'
nren_id = sa.Column(
sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
fee_type = sa.Column('fee_type', sa.Enum("flat_fee", "usage_based_fee",
"combination", "no_charge",
"other",
name="fee_type"), nullable=True)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
fee_type: Mapped[Optional[FeeType]]
class NrenStaff(base_schema):
class NrenStaff(Base):
__tablename__ = 'nren_staff'
nren_id = sa.Column(sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
permanent_fte = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
subcontracted_fte = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
technical_fte = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
non_technical_fte = sa.Column(sa.Numeric(asdecimal=False), nullable=False)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
permanent_fte: Mapped[Decimal]
subcontracted_fte: Mapped[Decimal]
technical_fte: Mapped[Decimal]
non_technical_fte: Mapped[Decimal]
class ParentOrganization(base_schema):
class ParentOrganization(Base):
__tablename__ = 'parent_organization'
nren_id = sa.Column(sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
organization = sa.Column(sa.String(128), nullable=False)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
organization: Mapped[str128]
class SubOrganization(base_schema):
class SubOrganization(Base):
__tablename__ = 'sub_organization'
nren_id = sa.Column(sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
organization = sa.Column(sa.String(128), primary_key=True)
role = sa.Column(sa.String(128), nullable=False)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(lazy='joined')
year: Mapped[int_pk]
organization: Mapped[str128] = mapped_column(primary_key=True)
role: Mapped[str128]
class ECProject(base_schema):
class ECProject(Base):
__tablename__ = 'ec_project'
nren_id = sa.Column(sa.Integer, sa.schema.ForeignKey(NREN.id), primary_key=True)
nren = relationship(NREN, lazy='joined')
year = sa.Column(sa.Integer, primary_key=True)
project = sa.Column(sa.String(256), primary_key=True)
nren_id: Mapped[int_pk_fkNREN]
nren: Mapped[NREN] = relationship(NREN, lazy='joined')
year: Mapped[int_pk]
project: Mapped[str] = mapped_column(String(256), primary_key=True)
......@@ -7,8 +7,7 @@ LOGGING_DEFAULT_CONFIG = {
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(asctime)s - %(name)s '
'(%(lineno)d) - %(levelname)s - %(message)s'
'format': '%(asctime)s - %(name)s (%(lineno)d) - %(levelname)s - %(message)s'
}
},
......
Single-database configuration for Flask.
......@@ -4,7 +4,7 @@ from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from compendium_v2.db.model import base_schema
from compendium_v2.db.model import metadata_obj
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
......@@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO)
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = base_schema.metadata
target_metadata = metadata_obj
# other values from the config, defined by the needs of env.py,
# can be acquired:
......
"""
survey_publisher_2022
=========================
This module loads the survey data from 2022 from the survey database.
Registered as click cli command when installing compendium-v2.
"""
import logging
import click
import enum
......@@ -136,15 +144,11 @@ def transfer_budget():
try:
budget = float(_budget.replace('"', '').replace(',', ''))
except ValueError:
logger.info(
f'{nren_name} has no budget for 2022. Skipping.'
f' ({_budget}))')
logger.info(f'{nren_name} has no budget for 2022. Skipping. ({_budget}))')
continue
if budget > 200:
logger.info(
f'{nren_name} has budget set to >200M EUR for 2022.'
f' ({budget})')
logger.info(f'{nren_name} has budget set to >200M EUR for 2022. ({budget})')
if nren_name not in nren_dict:
logger.info(f'{nren_name} unknown. Skipping.')
......@@ -172,9 +176,7 @@ def transfer_funding_sources():
value = float(_value.replace('"', '').replace(',', ''))
except ValueError:
name = source.name
logger.info(
f'{nren_name} has invalid value for {name}.'
+ f' ({_value}))')
logger.info(f'{nren_name} has invalid value for {name}. ({_value}))')
value = 0
nren_info = sourcedata.setdefault(
......@@ -196,8 +198,7 @@ def transfer_funding_sources():
funding_source = model.FundingSource(
nren=nren_dict[nren_name],
year=2022,
client_institutions=nren_info[
FundingSource.CLIENT_INSTITUTIONS],
client_institutions=nren_info[FundingSource.CLIENT_INSTITUTIONS],
european_funding=nren_info[FundingSource.EUROPEAN_FUNDING],
gov_public_bodies=nren_info[FundingSource.GOV_PUBLIC_BODIES],
commercial=nren_info[FundingSource.COMMERCIAL],
......@@ -241,11 +242,8 @@ def transfer_staff_data():
employed = nren_info[StaffQuestion.PERMANENT_FTE] + nren_info[StaffQuestion.SUBCONTRACTED_FTE]
technical = nren_info[StaffQuestion.TECHNICAL_FTE] + nren_info[StaffQuestion.NON_TECHNICAL_FTE]
if not math.isclose(
employed,
technical,
abs_tol=0.01,
):
if not math.isclose(employed, technical, abs_tol=0.01):
logger.info(f'{nren_name} FTE do not equal across employed/technical categories.'
f' ({employed} != {technical})')
......@@ -354,15 +352,15 @@ def transfer_charging_structure():
continue
if "do not charge" in value:
charging_structure = FeeType.no_charge.value
charging_structure = FeeType.no_charge
elif "combination" in value:
charging_structure = FeeType.combination.value
charging_structure = FeeType.combination
elif "flat" in value:
charging_structure = FeeType.flat_fee.value
charging_structure = FeeType.flat_fee
elif "usage-based" in value:
charging_structure = FeeType.usage_based_fee.value
charging_structure = FeeType.usage_based_fee
elif "Other" in value:
charging_structure = FeeType.other.value
charging_structure = FeeType.other
else:
charging_structure = None
......
"""
survey_publisher_v1
=========================
This module loads the survey data from before 2022 from and excel file.
Missing info is filled in from the survey db for some questions.
Registered as click cli command when installing compendium-v2.
"""
import logging
import math
import click
......@@ -29,10 +38,7 @@ def db_budget_migration():
year = budget.year
if float(budget.budget) > 200:
logger.info(
f'Incorrect Data: '
f'{abbrev} has budget set to '
f'>200M EUR for {year}. ({budget.budget})')
logger.info(f'Incorrect Data: {abbrev} has budget set to >200M EUR for {year}. ({budget.budget})')
if abbrev not in nren_dict:
logger.info(f'{abbrev} unknown. Skipping.')
......@@ -53,8 +59,7 @@ def db_budget_migration():
logger.info(f'{abbrev} unknown. Skipping.')
continue
budget_entry = model.BudgetEntry(
nren=nren_dict[abbrev], budget=budget, year=year)
budget_entry = model.BudgetEntry(nren=nren_dict[abbrev], budget=budget, year=year)
session.merge(budget_entry)
session.commit()
......@@ -71,13 +76,10 @@ def db_funding_migration():
gov_public_bodies,
commercial, other) in data:
_data = [client_institution, european_funding,
gov_public_bodies, commercial, other]
_data = [client_institution, european_funding, gov_public_bodies, commercial, other]
total = sum(_data)
if not math.isclose(total, 100, abs_tol=0.01):
logger.info(
f'{abbrev} funding sources for {year}'
f' do not sum to 100% ({total})')
logger.info(f'{abbrev} funding sources for {year} do not sum to 100% ({total})')
if abbrev not in nren_dict:
logger.info(f'{abbrev} unknown. Skipping.')
......
"""
API Endpoints
=========================
.. contents:: :local:
/api/
---------------------
"""
import logging
from flask import Blueprint
......
......@@ -57,7 +57,7 @@ def charging_structure_view() -> Any:
return {
'NREN': entry.nren.name,
'YEAR': int(entry.year),
'FEE_TYPE': entry.fee_type,
'FEE_TYPE': entry.fee_type.value if entry.fee_type is not None else None,
}
with db.session_scope() as session:
......
"""
Default Endpoints
=========================
.. contents:: :local:
/version
---------------------
.. autofunction:: compendium_v2.routes.default.version
"""
import pkg_resources
from flask import Blueprint, jsonify, render_template, Response
......
......@@ -62,7 +62,7 @@ def funding_source_view() -> Any:
def _extract_data(entry: model.FundingSource):
return {
'NREN': entry.nren.name,
'YEAR': int(entry.year),
'YEAR': entry.year,
'CLIENT_INSTITUTIONS': float(entry.client_institutions),
'EUROPEAN_FUNDING': float(entry.european_funding),
'GOV_PUBLIC_BODIES': float(entry.gov_public_bodies),
......
......@@ -61,10 +61,10 @@ def staff_view() -> Any:
return {
'nren': entry.nren.name,
'year': entry.year,
'permanent_fte': entry.permanent_fte,
'subcontracted_fte': entry.subcontracted_fte,
'technical_fte': entry.technical_fte,
'non_technical_fte': entry.non_technical_fte
'permanent_fte': float(entry.permanent_fte),
'subcontracted_fte': float(entry.subcontracted_fte),
'technical_fte': float(entry.technical_fte),
'non_technical_fte': float(entry.non_technical_fte)
}
with db.session_scope() as session:
......
import logging
import sqlalchemy as sa
from typing import List, Optional
from typing import Any
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy.schema import ForeignKey
logger = logging.getLogger(__name__)
# https://github.com/python/mypy/issues/2477
base_schema: Any = declarative_base()
class Base(DeclarativeBase):
pass
class Budgets(base_schema):
class Budgets(Base):
__tablename__ = 'budgets'
id = sa.Column(sa.Integer, primary_key=True)
budget = sa.Column(sa.String)
year = sa.Column(sa.Integer)
country_code = sa.Column('country_code', sa.String,
sa.ForeignKey('nrens.country_code'))
nren = relationship('Nrens', back_populates='budgets')
id: Mapped[int] = mapped_column(primary_key=True)
budget: Mapped[Optional[str]]
year: Mapped[Optional[int]]
country_code: Mapped[Optional[str]] = mapped_column(ForeignKey('nrens.country_code'))
nren: Mapped[Optional['Nrens']] = relationship(back_populates='budgets')
class Nrens(base_schema):
class Nrens(Base):
__tablename__ = 'nrens'
id = sa.Column(sa.Integer, primary_key=True)
abbreviation = sa.Column(sa.String)
country_code = sa.Column(sa.String)
budgets = relationship('Budgets', back_populates='nren')
id: Mapped[int] = mapped_column(primary_key=True)
abbreviation: Mapped[Optional[str]]
country_code: Mapped[Optional[str]]
budgets: Mapped[List['Budgets']] = relationship(back_populates='nren')
......@@ -9,14 +9,8 @@ Responses to valid requests are returned as JSON messages.
The server will therefore return an error unless
`application/json` is in the `Accept` request header field.
HTTP communication and JSON grammar details are
beyond the scope of this document.
Please refer to [RFC 2616](https://tools.ietf.org/html/rfc2616)
and www.json.org for more details.
.. contents:: :local:
.. automodule:: compendium_v2.routes.default
.. automodule:: compendium_v2.routes.api
.. qrefflask:: compendium_v2:_create_app(None)
:autoquickref:
.. autoflask:: compendium_v2:_create_app(None)
:undoc-static:
......@@ -73,7 +73,9 @@ release = '0.0'
extensions = [
'sphinx_rtd_theme',
'sphinx.ext.autodoc',
'sphinx.ext.coverage'
'sphinx.ext.coverage',
'sphinxcontrib.autohttp.flask',
'sphinxcontrib.autohttp.flaskqref'
]
# Add any paths that contain templates here, relative to this directory.
......
......@@ -11,3 +11,4 @@ a React web application that consumes and renders the json data.
api
development
publishers
.. api intro
Publishers
===============
.. contents:: :local:
.. automodule:: compendium_v2.publishers.survey_publisher_v1
:members:
.. automodule:: compendium_v2.publishers.survey_publisher_2022
:members:
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment