diff --git a/compendium_v2/__init__.py b/compendium_v2/__init__.py index 33868c28bf74ea7320e1bfeae682b43611f0bb73..622565f2f7a3a5d39c1df9b562e11bd336dfc230 100644 --- a/compendium_v2/__init__.py +++ b/compendium_v2/__init__.py @@ -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') diff --git a/compendium_v2/background_task/parse_excel_data.py b/compendium_v2/background_task/parse_excel_data.py index d599f477a46cebb163f19a87d4559aaafcef8543..ef8e2cd61d023df86d53cf534d93db83e84a7709 100644 --- a/compendium_v2/background_task/parse_excel_data.py +++ b/compendium_v2/background_task/parse_excel_data.py @@ -1,6 +1,6 @@ +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 diff --git a/compendium_v2/config.py b/compendium_v2/config.py index 30ee63881344dc13080fb3664d838532302b85a8..5cf1a69b3527529672a25ed6af7130fd4a14cb0e 100644 --- a/compendium_v2/config.py +++ b/compendium_v2/config.py @@ -1,32 +1,13 @@ 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 diff --git a/compendium_v2/db/model.py b/compendium_v2/db/model.py index d1b04a0ac1a74b669a79bbe09233226431d15943..676727437984fcf4b0160d91060dfcc343c376c8 100644 --- a/compendium_v2/db/model.py +++ b/compendium_v2/db/model.py @@ -1,12 +1,13 @@ 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) diff --git a/compendium_v2/environment.py b/compendium_v2/environment.py index 55cfae65ac4f75532215fc03b551c93aa522a489..527687c42d497d6ec64a15c4b1eabfad9244db1c 100644 --- a/compendium_v2/environment.py +++ b/compendium_v2/environment.py @@ -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' } }, diff --git a/compendium_v2/migrations/README b/compendium_v2/migrations/README deleted file mode 100644 index 0e048441597444a7e2850d6d7c4ce15550f79bda..0000000000000000000000000000000000000000 --- a/compendium_v2/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Single-database configuration for Flask. diff --git a/compendium_v2/migrations/env.py b/compendium_v2/migrations/env.py index 5ae69c65ba9b2c844a16e54c9f03d4b0297ae68b..0307be3377e7eb332145d5822b4fb126b6f96e59 100644 --- a/compendium_v2/migrations/env.py +++ b/compendium_v2/migrations/env.py @@ -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: diff --git a/compendium_v2/publishers/survey_publisher_2022.py b/compendium_v2/publishers/survey_publisher_2022.py index 6334fae31446d600bfd0ece3dccc50d436524a2b..710c899d4378a7e5a9bd76246789176fb2d57e54 100644 --- a/compendium_v2/publishers/survey_publisher_2022.py +++ b/compendium_v2/publishers/survey_publisher_2022.py @@ -1,3 +1,11 @@ +""" +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 diff --git a/compendium_v2/publishers/survey_publisher_v1.py b/compendium_v2/publishers/survey_publisher_v1.py index 297ec047f1dba15b44487efeaca9dfcb5b484346..d07f1f42f32f0cf9315faa3e0d0c08c7dfab4792 100644 --- a/compendium_v2/publishers/survey_publisher_v1.py +++ b/compendium_v2/publishers/survey_publisher_v1.py @@ -1,3 +1,12 @@ +""" +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.') diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py index e4fd41042a065ed283dd21b2b3da84f0e749bd43..ec39ab773ee426a387b49911a7399dd3b82662d2 100644 --- a/compendium_v2/routes/api.py +++ b/compendium_v2/routes/api.py @@ -1,15 +1,3 @@ -""" -API Endpoints -========================= - -.. contents:: :local: - -/api/ ---------------------- - - - -""" import logging from flask import Blueprint diff --git a/compendium_v2/routes/charging.py b/compendium_v2/routes/charging.py index eefeb017223196e526bc959fff4a45206cd5992e..0b2c238414bda0f4460762bb48a27464a2101f3c 100644 --- a/compendium_v2/routes/charging.py +++ b/compendium_v2/routes/charging.py @@ -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: diff --git a/compendium_v2/routes/default.py b/compendium_v2/routes/default.py index eaf1420685079ac49119b3a84355df8f49d752c8..9d2ed2f9bef6e6e8d742a7d8ad02d41d99f90f5d 100644 --- a/compendium_v2/routes/default.py +++ b/compendium_v2/routes/default.py @@ -1,15 +1,3 @@ -""" -Default Endpoints -========================= - -.. contents:: :local: - -/version ---------------------- - -.. autofunction:: compendium_v2.routes.default.version - -""" import pkg_resources from flask import Blueprint, jsonify, render_template, Response diff --git a/compendium_v2/routes/funding.py b/compendium_v2/routes/funding.py index 0e504355593efd1a37c2ee490af810a1732d71a5..ed0e26c8809b0b3f8187af5a8afe25ed2ec16010 100644 --- a/compendium_v2/routes/funding.py +++ b/compendium_v2/routes/funding.py @@ -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), diff --git a/compendium_v2/routes/staff.py b/compendium_v2/routes/staff.py index 0a57d57a87d0486d09d37d9f70fe1abc77d37d72..73e79c4836bb42e2959ae41a22c1b094946e0f29 100644 --- a/compendium_v2/routes/staff.py +++ b/compendium_v2/routes/staff.py @@ -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: diff --git a/compendium_v2/survey_db/model.py b/compendium_v2/survey_db/model.py index 45c27ddf82a2854799190b438c31576d3c5126dc..a908aa201aaae8c194614ece4ab067aa1664a43b 100644 --- a/compendium_v2/survey_db/model.py +++ b/compendium_v2/survey_db/model.py @@ -1,30 +1,28 @@ 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') diff --git a/docs/source/api.rst b/docs/source/api.rst index 1db2969abd99b080c252bb06be8b75e5b7572c82..bd4f24693c5a6e754886afa585b3e4d04945c0f3 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -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: diff --git a/docs/source/conf.py b/docs/source/conf.py index 48b40e921dbb72df8f82c3604f36a250bd557c14..62ba0d569569f45347cb9c6a3974ab948a79b3f7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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. diff --git a/docs/source/index.rst b/docs/source/index.rst index 11a4f0a350ea8385478ed15053622c3051f13aaa..8a97f8f6cd34c7c6df1d3997d21b46fc04f798c9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,3 +11,4 @@ a React web application that consumes and renders the json data. api development + publishers diff --git a/docs/source/publishers.rst b/docs/source/publishers.rst new file mode 100644 index 0000000000000000000000000000000000000000..85818b3f9a1370bacff99e806de11ca1951248bc --- /dev/null +++ b/docs/source/publishers.rst @@ -0,0 +1,13 @@ +.. api intro + +Publishers +=============== + + +.. contents:: :local: + +.. automodule:: compendium_v2.publishers.survey_publisher_v1 + :members: + +.. automodule:: compendium_v2.publishers.survey_publisher_2022 + :members: diff --git a/requirements.txt b/requirements.txt index 782ca2f83404d84121f60e8351c2cf8c83e5e380..f409d9fa4cb574651214e8f455436d4298aef041 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,19 +6,19 @@ flask-cors psycopg2-binary cryptography SQLAlchemy +openpyxl pytest pytest-mock python-dotenv sphinx sphinx-rtd-theme +sphinxcontrib-httpdomain tox mypy types-docutils types-jsonschema types-Flask-Cors +types-openpyxl types-setuptools -types-sqlalchemy - -openpyxl diff --git a/test/conftest.py b/test/conftest.py index a2507e0fea6df752d96c351fa535762ab8831462..79186212deb7f5bf06fdd61df781349e5574ddff 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -18,10 +18,7 @@ import csv def _test_data_csv(filename): - data_filename = os.path.join( - os.path.dirname(__file__), - 'data', - filename) + data_filename = os.path.join(os.path.dirname(__file__), 'data', filename) yield from csv.DictReader(open(data_filename, "r")) @@ -40,13 +37,9 @@ def mocked_survey_db(mocker): connect_args={'check_same_thread': False}, poolclass=StaticPool, echo=False) - survey_model.base_schema.metadata.create_all(engine) - mocker.patch( - 'compendium_v2.survey_db._SESSION_MAKER', - sessionmaker(bind=engine)) - mocker.patch( - 'compendium_v2.survey_db.init_db_model', - lambda dsn: None) + survey_model.Base.metadata.create_all(engine) + mocker.patch('compendium_v2.survey_db._SESSION_MAKER', sessionmaker(bind=engine)) + mocker.patch('compendium_v2.survey_db.init_db_model', lambda dsn: None) @pytest.fixture @@ -57,16 +50,10 @@ def mocked_db(mocker): connect_args={'check_same_thread': False}, poolclass=StaticPool, echo=False) - model.base_schema.metadata.create_all(engine) - mocker.patch( - 'compendium_v2.db._SESSION_MAKER', - sessionmaker(bind=engine)) - mocker.patch( - 'compendium_v2.db.init_db_model', - lambda dsn: None) - mocker.patch( - 'compendium_v2.migrate_database', - lambda config: None) + model.Base.metadata.create_all(engine) + mocker.patch('compendium_v2.db._SESSION_MAKER', sessionmaker(bind=engine)) + mocker.patch('compendium_v2.db.init_db_model', lambda dsn: None) + mocker.patch('compendium_v2.migrate_database', lambda config: None) @pytest.fixture @@ -74,8 +61,7 @@ def test_budget_data(): with db.session_scope() as session: data = [row for row in _test_data_csv("BudgetTestData.csv")] nren_names = set([row["nren"] for row in data]) - nren_dict = { - nren_name: model.NREN(name=nren_name) for nren_name in nren_names} + nren_dict = {nren_name: model.NREN(name=nren_name) for nren_name in nren_names} session.add_all(nren_dict.values()) for row in data: @@ -83,12 +69,8 @@ def test_budget_data(): budget = row["budget"] year = row["year"] - session.add( - model.BudgetEntry( - nren=nren, - budget=float(budget), - year=int(year)) - ) + session.add(model.BudgetEntry(nren=nren, budget=float(budget), year=int(year))) + with survey_db.session_scope() as session: data = _test_data_csv("BudgetTestData.csv") nrens = set() @@ -101,19 +83,10 @@ def test_budget_data(): nrens.add(nren) - budgets_data.append( - survey_model.Budgets( - budget=budget, - year=year, - country_code=country_code - ) - ) + budgets_data.append(survey_model.Budgets(budget=budget, year=year, country_code=country_code)) + for nren in nrens: - session.add( - survey_model.Nrens( - abbreviation=nren, - country_code=nren - )) + session.add(survey_model.Nrens(abbreviation=nren, country_code=nren)) session.add_all(budgets_data) @@ -123,8 +96,7 @@ def test_funding_source_data(): with db.session_scope() as session: data = [row for row in _test_data_csv("FundingSourceTestData.csv")] nren_names = set([row["nren"] for row in data]) - nren_dict = { - nren_name: model.NREN(name=nren_name) for nren_name in nren_names} + nren_dict = {nren_name: model.NREN(name=nren_name) for nren_name in nren_names} session.add_all(nren_dict.values()) for row in data: @@ -167,8 +139,7 @@ def test_staff_data(): data = list(_generate_rows()) - nren_dict = { - nren_name: model.NREN(name=nren_name) for nren_name in [d['nren'] for d in data]} + nren_dict = {nren_name: model.NREN(name=nren_name) for nren_name in [d['nren'] for d in data]} session.add_all(nren_dict.values()) for row in data: @@ -211,8 +182,7 @@ def test_charging_structure_data(): with db.session_scope() as session: data = [row for row in _test_data_csv("ChargingStructureTestData.csv")] nren_names = set([row["nren"] for row in data]) - nren_dict = { - nren_name: model.NREN(name=nren_name) for nren_name in nren_names} + nren_dict = {nren_name: model.NREN(name=nren_name) for nren_name in nren_names} session.add_all(nren_dict.values()) for row in data: @@ -264,27 +234,16 @@ def test_organization_data(): year = org["year"] name = org["name"] - session.add( - model.ParentOrganization( - nren=nren, - year=year, - organization=name - ) - ) + session.add(model.ParentOrganization(nren=nren, year=year, organization=name)) + for sub_org in sub_org_data: nren = nren_dict[sub_org["nren"]] year = sub_org["year"] name = sub_org["name"] role = sub_org["role"] - session.add( - model.SubOrganization( - nren=nren, - year=year, - organization=name, - role=role - ) - ) + session.add(model.SubOrganization(nren=nren, year=year, organization=name, role=role)) + session.commit() @@ -318,11 +277,6 @@ def test_ec_project_data(): year = ec_project["year"] project = ec_project["project"] - session.add( - model.ECProject( - nren=nren, - year=year, - project=project - ) - ) + session.add(model.ECProject(nren=nren, year=year, project=project)) + session.commit() diff --git a/test/test_survey_publisher_2022.py b/test/test_survey_publisher_2022.py index 0ec89e6ed4f9b148d93cb546b27ff0ccd6bd59e7..ec8802edec3d0a3f16d7e62cb01277dcce554ed8 100644 --- a/test/test_survey_publisher_2022.py +++ b/test/test_survey_publisher_2022.py @@ -182,36 +182,23 @@ def test_publisher(client, mocker, dummy_config): ('nren3', '["project3"]'), ] - mocker.patch('compendium_v2.publishers.survey_publisher_2022.query_budget', - get_rows_as_tuples) - - mocker.patch( - 'compendium_v2.publishers.survey_publisher_2022.query_funding_sources', - funding_source_data) - - mocker.patch( - 'compendium_v2.publishers.survey_publisher_2022.query_question', - question_data) + mocker.patch('compendium_v2.publishers.survey_publisher_2022.query_budget', get_rows_as_tuples) + mocker.patch('compendium_v2.publishers.survey_publisher_2022.query_funding_sources', funding_source_data) + mocker.patch('compendium_v2.publishers.survey_publisher_2022.query_question', question_data) with db.session_scope() as session: - nren_names = [ - 'Nren1', 'Nren2', 'Nren3', 'Nren4', - 'SURF', 'KIFU', 'UoM', 'ASNET-AM', 'SIKT', 'LAT', 'RASH' - ] - session.add_all( - [model.NREN(name=nren_name) for nren_name in nren_names]) + nren_names = ['Nren1', 'Nren2', 'Nren3', 'Nren4', 'SURF', 'KIFU', 'UoM', 'ASNET-AM', 'SIKT', 'LAT', 'RASH'] + session.add_all([model.NREN(name=nren_name) for nren_name in nren_names]) _cli(dummy_config) with db.session_scope() as session: - budgets = session.query(model.BudgetEntry).order_by( - model.BudgetEntry.nren_id.asc()).all() + budgets = session.query(model.BudgetEntry).order_by(model.BudgetEntry.nren_id.asc()).all() assert len(budgets) == 3 assert budgets[0].nren.name.lower() == 'nren1' assert budgets[0].budget == 100 - funding_sources = session.query(model.FundingSource).order_by( - model.FundingSource.nren_id.asc()).all() + funding_sources = session.query(model.FundingSource).order_by(model.FundingSource.nren_id.asc()).all() assert len(funding_sources) == 3 assert funding_sources[0].nren.name.lower() == 'nren1' assert funding_sources[0].client_institutions == 10 @@ -228,8 +215,7 @@ def test_publisher(client, mocker, dummy_config): assert funding_sources[2].european_funding == 30 assert funding_sources[2].other == 30 - staff_data = session.query(model.NrenStaff).order_by( - model.NrenStaff.nren_id.asc()).all() + staff_data = session.query(model.NrenStaff).order_by(model.NrenStaff.nren_id.asc()).all() assert len(staff_data) == 3 assert staff_data[0].nren.name.lower() == 'nren1' @@ -250,8 +236,7 @@ def test_publisher(client, mocker, dummy_config): assert staff_data[2].permanent_fte == 30 assert staff_data[2].subcontracted_fte == 0 - _org_data = session.query(model.ParentOrganization).order_by( - model.ParentOrganization.nren_id.asc()).all() + _org_data = session.query(model.ParentOrganization).order_by(model.ParentOrganization.nren_id.asc()).all() assert len(_org_data) == 2 assert _org_data[0].nren.name.lower() == 'nren1' @@ -264,14 +249,13 @@ def test_publisher(client, mocker, dummy_config): model.ChargingStructure.nren_id.asc()).all() assert len(charging_structures) == 3 assert charging_structures[0].nren.name.lower() == 'nren1' - assert charging_structures[0].fee_type == 'no_charge' + assert charging_structures[0].fee_type == model.FeeType.no_charge assert charging_structures[1].nren.name.lower() == 'nren2' - assert charging_structures[1].fee_type == 'usage_based_fee' + assert charging_structures[1].fee_type == model.FeeType.usage_based_fee assert charging_structures[2].nren.name.lower() == 'nren3' - assert charging_structures[2].fee_type == 'other' + assert charging_structures[2].fee_type == model.FeeType.other - _ec_data = session.query(model.ECProject).order_by( - model.ECProject.nren_id.asc()).all() + _ec_data = session.query(model.ECProject).order_by(model.ECProject.nren_id.asc()).all() assert len(_ec_data) == 3 assert _ec_data[0].nren.name.lower() == 'nren2' diff --git a/test/test_survey_publisher_v1.py b/test/test_survey_publisher_v1.py index 79c809316d713da2223af1c77c069b5cec06f85e..6908b69dd374938ea82532470e653dc35426df2b 100644 --- a/test/test_survey_publisher_v1.py +++ b/test/test_survey_publisher_v1.py @@ -4,19 +4,15 @@ from compendium_v2 import db from compendium_v2.db import model from compendium_v2.publishers.survey_publisher_v1 import _cli -EXCEL_FILE = os.path.join( - os.path.dirname(__file__), "data", - "2021_Organisation_DataSeries.xlsx") +EXCEL_FILE = os.path.join(os.path.dirname(__file__), "data", "2021_Organisation_DataSeries.xlsx") def test_publisher(client, mocker, dummy_config): - mocker.patch('compendium_v2.background_task.parse_excel_data.EXCEL_FILE', - EXCEL_FILE) + mocker.patch('compendium_v2.background_task.parse_excel_data.EXCEL_FILE', EXCEL_FILE) with db.session_scope() as session: nren_names = ['SURF', 'KIFU', 'UoM', 'ASNET-AM', 'SIKT', 'LAT', 'RASH'] - session.add_all( - [model.NREN(name=nren_name) for nren_name in nren_names]) + session.add_all([model.NREN(name=nren_name) for nren_name in nren_names]) _cli(dummy_config) @@ -25,6 +21,5 @@ def test_publisher(client, mocker, dummy_config): assert budget_count funding_source_count = session.query(model.FundingSource.year).count() assert funding_source_count - charging_structure_count = session.query(model.ChargingStructure.year)\ - .count() + charging_structure_count = session.query(model.ChargingStructure.year).count() assert charging_structure_count