From da1f649e0a3ecbe20e5bcef8eeacdf41a6cb5ea4 Mon Sep 17 00:00:00 2001 From: Mohammad Torkashvand <mohammad.torkashvand@geant.org> Date: Fri, 15 Sep 2023 17:25:35 +0200 Subject: [PATCH 1/7] service matrix prototype --- compendium_v2/conversion/mapping.py | 13 + compendium_v2/db/presentation_models.py | 28 +- ...ab_added_service_and_nrenservice_tables.py | 50 +++ compendium_v2/publishers/excel_parser.py | 73 +++- .../survey_publisher_legacy_excel.py | 45 ++- compendium_v2/resources/__init__.py | 6 + compendium_v2/resources/nren_services.json | 372 ++++++++++++++++++ compendium_v2/routes/api.py | 2 + compendium_v2/routes/nren_services.py | 58 +++ test/conftest.py | 55 +++ test/test_nren_services.py | 14 + test/test_survey_publisher_legacy_excel.py | 5 +- ...y => test_survey_publisher_old_db_2022.py} | 0 13 files changed, 704 insertions(+), 17 deletions(-) create mode 100644 compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py create mode 100644 compendium_v2/resources/__init__.py create mode 100644 compendium_v2/resources/nren_services.json create mode 100644 compendium_v2/routes/nren_services.py create mode 100644 test/test_nren_services.py rename test/{test_db_survey_publisher_2022.py => test_survey_publisher_old_db_2022.py} (100%) diff --git a/compendium_v2/conversion/mapping.py b/compendium_v2/conversion/mapping.py index 1258da6c..4a4f7df8 100644 --- a/compendium_v2/conversion/mapping.py +++ b/compendium_v2/conversion/mapping.py @@ -1,3 +1,5 @@ +from compendium_v2.db.presentation_models import ServiceCategory + ANSWERS_2022_QUERY = """ SELECT question_id, value FROM answers a @@ -664,3 +666,14 @@ SERVICES_MAPPING = { 'SaaS': 'services_hosting:saas', 'Virtual machines/IaaS': 'services_hosting:virtual-machines-iaas', } + +SERVICE_CATEGORY_MAPPING = { + 'services_collaboration': ServiceCategory.collaboration.value, + 'services_identity': ServiceCategory.identity.value, + 'services_isp': ServiceCategory.isp_support.value, + 'services_multimedia': ServiceCategory.multimedia.value, + 'services_network': ServiceCategory.network_services.value, + 'services_professional': ServiceCategory.professional_services.value, + 'services_security': ServiceCategory.security.value, + 'services_hosting': ServiceCategory.storage_and_hosting.value, +} diff --git a/compendium_v2/db/presentation_models.py b/compendium_v2/db/presentation_models.py index de573493..81de9bc1 100644 --- a/compendium_v2/db/presentation_models.py +++ b/compendium_v2/db/presentation_models.py @@ -6,7 +6,7 @@ from decimal import Decimal from typing import List, Optional from typing_extensions import Annotated, TypedDict -from sqlalchemy import String, JSON +from sqlalchemy import String, JSON, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.schema import ForeignKey @@ -22,10 +22,13 @@ logger = logging.getLogger(__name__) str128 = Annotated[str, 128] str128_pk = Annotated[str, mapped_column(String(128), primary_key=True)] str256_pk = Annotated[str, mapped_column(String(256), primary_key=True)] +str128_unique = Annotated[str, mapped_column(String(128), unique=True)] int_pk = Annotated[int, mapped_column(primary_key=True)] +str_text = Annotated[str, mapped_column(Text)] int_pk_fkNREN = Annotated[int, mapped_column(ForeignKey("nren.id"), primary_key=True)] user_category_pk = Annotated[UserCategory, mapped_column(primary_key=True)] json_str_list = Annotated[List[str], mapped_column(JSON)] +int_pk_fkService = Annotated[int, mapped_column(ForeignKey("service.id"), primary_key=True)] ExternalConnection = TypedDict( 'ExternalConnection', @@ -471,3 +474,26 @@ class NetworkAutomation(db.Model): year: Mapped[int_pk] network_automation: Mapped[YesNoPlanned] network_automation_specifics: Mapped[json_str_list] + + +class Service(db.Model): + __tablename__ = 'service' + + id: Mapped[int_pk] + name_key: Mapped[str128_unique] + name: Mapped[str128] + category: Mapped[ServiceCategory] + description: Mapped[str_text] + + +class NRENService(db.Model): + __tablename__ = 'nren_service' + + nren_id: Mapped[int_pk_fkNREN] + nren: Mapped[NREN] = relationship(lazy='joined') + year: Mapped[int_pk] + service_id: Mapped[int_pk_fkService] + service: Mapped[Service] = relationship(lazy='joined') + product_name: Mapped[str128] + additional_information: Mapped[str_text] + official_description: Mapped[str_text] diff --git a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py new file mode 100644 index 00000000..1c16c6a9 --- /dev/null +++ b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py @@ -0,0 +1,50 @@ +"""added Service and NRENService tables + +Revision ID: 1fbc4582c0ab +Revises: e17f9e6c90ab +Create Date: 2023-09-15 16:54:34.700341 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '1fbc4582c0ab' +down_revision = 'e17f9e6c90ab' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').create(op.get_bind()) + op.create_table('service', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name_key', sa.String(length=128), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('category', postgresql.ENUM('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory', create_type=False), nullable=False), + sa.Column('description', sa.Text(), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_service')), + sa.UniqueConstraint('name_key', name=op.f('uq_service_name_key')) + ) + op.create_table('nren_service', + sa.Column('nren_id', sa.Integer(), nullable=False), + sa.Column('year', sa.Integer(), nullable=False), + sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('product_name', sa.String(), nullable=False), + sa.Column('additional_information', sa.Text(), nullable=False), + sa.Column('official_description', sa.Text(), nullable=False), + sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_nren_service_nren_id_nren')), + sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_nren_service_service_id_service')), + sa.PrimaryKeyConstraint('nren_id', 'year', 'service_id', name=op.f('pk_nren_service')) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('nren_service') + op.drop_table('service') + sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').drop(op.get_bind()) + # ### end Alembic commands ### diff --git a/compendium_v2/publishers/excel_parser.py b/compendium_v2/publishers/excel_parser.py index 41f7c014..b842950b 100644 --- a/compendium_v2/publishers/excel_parser.py +++ b/compendium_v2/publishers/excel_parser.py @@ -1,22 +1,26 @@ import logging +from typing import Dict + import openpyxl -import os -from compendium_v2.db.presentation_model_enums import FeeType +from compendium_v2.conversion import mapping +from compendium_v2.db.presentation_models import FeeType from compendium_v2.environment import setup_logging +from compendium_v2.resources import get_resource_file_path setup_logging() logger = logging.getLogger(__name__) -EXCEL_FILE = os.path.join(os.path.dirname(__file__), "../resources", "2021_Organisation_DataSeries.xlsx") -NETWORK_EXCEL_FILE = os.path.join(os.path.dirname(__file__), "../resources", "2022_Networks_DataSeries.xlsx") +EXCEL_FILE_ORGANISATION = get_resource_file_path("2021_Organisation_DataSeries.xlsx") +EXCEL_FILE_NETWORKS = get_resource_file_path("2022_Networks_DataSeries.xlsx") +EXCEL_FILE_NREN_SERVICES = get_resource_file_path("NREN-Services-prefills_2023_Recovered.xlsx") def fetch_budget_excel_data(): # load the xlsx file sheet_name = "1. Budget" - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet ws = wb[sheet_name] @@ -36,7 +40,7 @@ def fetch_budget_excel_data(): def fetch_funding_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "2. Income Sources" @@ -119,7 +123,7 @@ def fetch_funding_excel_data(): def fetch_charging_structure_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "3. Charging mechanism" @@ -186,7 +190,7 @@ def fetch_charging_structure_excel_data(): def fetch_staffing_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "4. Staff" @@ -234,7 +238,7 @@ def fetch_staffing_excel_data(): def fetch_staff_function_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "5. Staff by Function" @@ -302,7 +306,7 @@ def fetch_staff_function_excel_data(): def fetch_ecproject_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "7. EC Projects" @@ -334,7 +338,7 @@ def fetch_ecproject_excel_data(): def fetch_organization_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_ORGANISATION, data_only=True, read_only=True) # select the active worksheet sheet_name = "Organization" @@ -352,7 +356,7 @@ def fetch_organization_excel_data(): def fetch_traffic_excel_data(): # load the xlsx file - wb = openpyxl.load_workbook(NETWORK_EXCEL_FILE, data_only=True, read_only=True) + wb = openpyxl.load_workbook(EXCEL_FILE_NETWORKS, data_only=True, read_only=True) # select the active worksheet sheet_name = "Estimated_Traffic TByte" @@ -392,3 +396,48 @@ def fetch_traffic_excel_data(): yield from create_points_for_year(2020, 14) yield from create_points_for_year(2021, 8) yield from create_points_for_year(2022, 2) + + +def fetch_nren_services_excel_data() -> Dict[str, str]: + wb = openpyxl.load_workbook(EXCEL_FILE_NREN_SERVICES, data_only=True, read_only=True) + ws = wb["Sheet1"] + rows = list(ws.rows) + + titles = rows[0] + + nren_service_data_columns = {} + + def normalize_nren_name(n: str) -> str: + n = n.split(' ')[0].upper() + return {'KIFÜ': 'KIFU', 'AZSCIENCENET': 'ANAS', 'PSNC': 'PIONIER'}.get(n, n) + + for i in range(0, 131): + if titles[i].value: + name = normalize_nren_name(titles[i].value) + nren_service_data_columns[name] = i + + for nren_name, start_column in nren_service_data_columns.items(): + for row_index in range(2, 61): + row = rows[row_index] + service_name = row[0].value + if row[start_column].value and row[start_column].value.upper() == 'YES': + product_name = '' + additional_information = '' + if row[start_column + 1].value: + product_name = row[start_column + 1].value + if row[start_column + 2].value: + additional_information = row[start_column + 2].value + + service_category, service_name_key = mapping.SERVICES_MAPPING[service_name].split(':') + service_category = mapping.SERVICE_CATEGORY_MAPPING[service_category] + + yield { + 'nren_name': nren_name, + 'service_name': service_name, + 'service_category': service_category, + 'service_name_key': service_name_key, + 'year': 2022, + 'product_name': product_name.strip(), + 'additional_information': additional_information.strip(), + 'official_description': '', + } diff --git a/compendium_v2/publishers/survey_publisher_legacy_excel.py b/compendium_v2/publishers/survey_publisher_legacy_excel.py index 6503b79f..8f41b196 100644 --- a/compendium_v2/publishers/survey_publisher_legacy_excel.py +++ b/compendium_v2/publishers/survey_publisher_legacy_excel.py @@ -8,18 +8,21 @@ Registered as click cli command when installing compendium-v2. """ from __future__ import annotations + +import json import logging import math -import click +import click from sqlalchemy import select import compendium_v2 -from compendium_v2.environment import setup_logging from compendium_v2.config import load from compendium_v2.db import db, presentation_models -from compendium_v2.survey_db import model as survey_model +from compendium_v2.environment import setup_logging from compendium_v2.publishers import helpers, excel_parser +from compendium_v2.resources import get_resource_file_path +from compendium_v2.survey_db import model as survey_model setup_logging() @@ -224,6 +227,40 @@ def db_traffic_volume_migration(nren_dict): db.session.commit() +def db_services_migration(): + with open(get_resource_file_path('nren_services.json')) as f: + services = json.load(f) + for name_key, record in services.items(): + service = presentation_models.Service( + name_key=name_key, + name=record['name'], + category=record['category'], + description=record['description'], + ) + db.session.merge(service) + db.session.commit() + + +def db_nren_services_migration(nren_dict): + for service_info in excel_parser.fetch_nren_services_excel_data(): + service = db.session.query(presentation_models.Service).filter_by( + name_key=service_info['service_name_key']).first() + + nren_service = presentation_models.NRENService( + nren=nren_dict[service_info['nren_name']], + nren_id=nren_dict[service_info['nren_name']].id, + year=service_info['year'], + service=service, + product_name=service_info['product_name'], + additional_information=service_info['additional_information'], + official_description=service_info['official_description'] + ) + + db.session.merge(nren_service) + + db.session.commit() + + def _cli(config, app): with app.app_context(): nren_dict = helpers.get_uppercase_nren_dict() @@ -234,6 +271,8 @@ def _cli(config, app): db_ecprojects_migration(nren_dict) db_organizations_migration(nren_dict) db_traffic_volume_migration(nren_dict) + db_services_migration() + db_nren_services_migration(nren_dict) @click.command() diff --git a/compendium_v2/resources/__init__.py b/compendium_v2/resources/__init__.py new file mode 100644 index 00000000..e2499aeb --- /dev/null +++ b/compendium_v2/resources/__init__.py @@ -0,0 +1,6 @@ +from pathlib import Path + + +def get_resource_file_path(filename: str) -> Path: + resource_dir = Path(__file__).resolve().parent + return resource_dir / filename diff --git a/compendium_v2/resources/nren_services.json b/compendium_v2/resources/nren_services.json new file mode 100644 index 00000000..29d14104 --- /dev/null +++ b/compendium_v2/resources/nren_services.json @@ -0,0 +1,372 @@ +{ + "anti-spam": { + "name": "Anti-spam solution", + "category": "security", + "description": "Anti-spam solutions for detecting and eliminating viruses and spam mails." + }, + "class-registration": { + "name": "Class registration tool", + "category": "collaboration", + "description": "Software for teachers to register students for classes etc." + }, + "cloud-service-end-user": { + "name": "Cloud storage (end user)", + "category": "storage_and_hosting", + "description": "Browser based virtual storage service for individuals." + }, + "connectivity": { + "name": "IP connectivity", + "category": "network_services", + "description": "Basic IP connectivity services inc. R+E and commodity internet" + }, + "consultancy": { + "name": "Consultancy/training", + "category": "professional_services", + "description": "Training and consultancy services provided by the NREN." + }, + "content-delivery-hosting": { + "name": "Content delivery hosting", + "category": "storage_and_hosting", + "description": "Hosting of content delivery servers e.g. Akamai." + }, + "content-management-system": { + "name": "CMS", + "category": "collaboration", + "description": "Provision of software systems for website authoring, collaboration and administration tools." + }, + "csirt": { + "name": "CERT/CSIRT", + "category": "security", + "description": "A single point of contact for users to deal with computer security incidents and prevention." + }, + "ddos-prevention": { + "name": "DDos mitigation", + "category": "security", + "description": "Tools and techniques for mitigating Distributed Denial of Service attacks." + }, + "domain-registration": { + "name": "Domain name registration", + "category": "isp_support", + "description": "Administration/registration of top and second level domain names." + }, + "eduroam-wifi": { + "name": "Eduroam", + "category": "identity", + "description": "Inter-WLAN service to facilitate easy and secure Internet access for roaming educational users." + }, + "email-services": { + "name": "Email server hosting", + "category": "collaboration", + "description": "NREN hosted email servers." + }, + "filesender": { + "name": "Filesender", + "category": "storage_and_hosting", + "description": "Web based application that allows authenticated users to securely and easily send arbitrarily large files." + }, + "firewall-on-demand": { + "name": "Firewall-on-demand", + "category": "security", + "description": "Provision of a dynamic firewall services to mitigate against DDOS attacks. " + }, + "home-vpn": { + "name": "Remote access VPN service", + "category": "network_services", + "description": "Remote access and site-to-site secure VPN." + }, + "aai": { + "name": "Hosted campus AAI", + "category": "identity", + "description": "Hosting of an Identity Provider service on behalf of connected institutions to authenticate users." + }, + "instant-messaging": { + "name": "Instant messaging", + "category": "collaboration", + "description": "Commercial or NREN own online chat service which offers real-time text transmission over the Internet." + }, + "interfederation": { + "name": "Interfederation", + "category": "identity", + "description": "Participation in an inter-federation (i.e. eduGAIN, KALMAR)." + }, + "internet-radio-tv": { + "name": "TV/radio streaming", + "category": "multimedia", + "description": "Internet and radio streaming services." + }, + "internship-database": { + "name": "Database services", + "category": "collaboration", + "description": "Provision of tools or services to create or host databases." + }, + "ip-address-allocation": { + "name": "IP address allocation/LIR", + "category": "isp_support", + "description": "Local Internet Registry service for assigning of IP address space." + }, + "ipv6": { + "name": "IPv6", + "category": "network_services", + "description": "The new version of the Internet Protocol (IP) that should eventually replace the current IP (version 4)." + }, + "ix-operation": { + "name": "National IX operation", + "category": "isp_support", + "description": "Operation of a national Internet Exchange." + }, + "journal-library-access": { + "name": "Journal access", + "category": "collaboration", + "description": "Access to academic journals." + }, + "lambda": { + "name": "Optical wavelength", + "category": "network_services", + "description": "Layer 1 optical channel for provision of dedicated capacity to demanding users." + }, + "mailing-lists": { + "name": "Mailing lists", + "category": "collaboration", + "description": "Service for operation of electronic discussion lists." + }, + "mobile-data": { + "name": "3G/4G data service", + "category": "network_services", + "description": "Provision of mobile broadband and phone contracts to users." + }, + "multicast": { + "name": "Multicast", + "category": "network_services", + "description": "Extension to the IP protocol which allows individual packets to be sent to multiple hosts on the Internet." + }, + "news-service": { + "name": "Netnews/USENET service", + "category": "storage_and_hosting", + "description": "Netnews/USENET news feed service." + }, + "online-payment": { + "name": "Online payment connectivity", + "category": "identity", + "description": "Connectivity for chip and pin payment terminals." + }, + "open-lightpath-exchange": { + "name": "Open Lightpath Exchange", + "category": "network_services", + "description": "Provision of an Open Lightpath exchange for users to connect to other parties." + }, + "pert": { + "name": "PERT", + "category": "network_services", + "description": "Team supporting resolution of end-to-end performance problems for networked applications." + }, + "plagarism-detection": { + "name": "Plagarism detection", + "category": "collaboration", + "description": "Provision of software for use by teachers etc to detect plagarism." + }, + "point-to-point-circuit-vpn": { + "name": "Virtual circuit/VPN", + "category": "network_services", + "description": "Virtual point to point circuits or VPNs delivered as a service to users." + }, + "procurement": { + "name": "Procurement/brokerage", + "category": "professional_services", + "description": "Procurement services, inc. negotiating agreements and framework agreements." + }, + "project-collaboration-toolkit": { + "name": "Project collaboration tools", + "category": "collaboration", + "description": "Packaged services for virtual project groups e.g. mailing lists, storage, web meetings, wiki." + }, + "quality-of-service": { + "name": "Quality of Service", + "category": "network_services", + "description": "Preferential service to specific applications or classes of applications." + }, + "scheduling-tool": { + "name": "Scheduling tool", + "category": "collaboration", + "description": "Provision of tools to users for scheduling appointments or classes." + }, + "sdn-testbed": { + "name": "SDN testbed", + "category": "network_services", + "description": "Software defined networking testbed available to users." + }, + "sms-messaging": { + "name": "SMS messaging", + "category": "collaboration", + "description": "Service for users to send or receive SMS message by email." + }, + "software-development": { + "name": "Software development", + "category": "professional_services", + "description": "Software development service for users." + }, + "software-licenses": { + "name": "Software licenses", + "category": "professional_services", + "description": "Provision of software for organisational or institutional purchase." + }, + "storage-co-location": { + "name": "Housing/co-location", + "category": "storage_and_hosting", + "description": "Hosting of user equipment in a managed data centre." + }, + "survey-tool": { + "name": "Survey/polling tool", + "category": "collaboration", + "description": "Provision of applications for creating surveys or polls." + }, + "system-backup": { + "name": "Hot standby", + "category": "storage_and_hosting", + "description": "Failover protection for primary web servers." + }, + "timeserver-ntp": { + "name": "NTP service", + "category": "isp_support", + "description": "Allows the synchronization of computer clocks over the Internet." + }, + "video-portal": { + "name": "Provision of content portal/s to users for hosting and viewing multi-media content.", + "category": "multimedia", + "description": "Multi-media content portal" + }, + "videoconferencing": { + "name": "Event recording/streaming", + "category": "multimedia", + "description": "Provision of equipment and/or software to support event streaming/recording." + }, + "virtual-learning-environment": { + "name": "VLE", + "category": "collaboration", + "description": "Online e-learning education system that provides virtual access to resources used in teaching.." + }, + "virtual-machines-iaas": { + "name": "Virtual machines/IaaS", + "category": "storage_and_hosting", + "description": "Access to virtual computing resources." + }, + "voip": { + "name": "VoIP", + "category": "collaboration", + "description": "Service to deliver voice communications and multimedia sessions over Internet Protocol (IP) networks." + }, + "vulnerability-testing": { + "name": "Vulnerability scanning", + "category": "security", + "description": "Vulnerability service that allows users to scan their own IP networks for security holes." + }, + "web-conferencing": { + "name": "Web/desktop conferencing", + "category": "multimedia", + "description": "Video conferencing service to desktops and hand-held devices using software." + }, + "web-design-production": { + "name": "Web development", + "category": "professional_services", + "description": "Web development service provided to NREN users." + }, + "web-email-hosting": { + "name": "Web hosting", + "category": "storage_and_hosting", + "description": "Service to provide space on central web servers for users to publish their website." + }, + "dns-server": { + "name": "DNS hosting", + "category": "storage_and_hosting", + "description": "Hosting of primary and secondary DNS servers." + }, + "dissemination": { + "name": "Dissemination", + "category": "professional_services", + "description": "Dissemination of information to users e.g. newsletters and magazines." + }, + "network-monitoring": { + "name": "Network monitoring", + "category": "network_services", + "description": "Network information system that shows the current and past performance of the network." + }, + "disaster-recovery": { + "name": "Disaster recovery", + "category": "storage_and_hosting", + "description": "Off site backup services." + }, + "user-monitoring": { + "name": "Network troubleshooting", + "category": "network_services", + "description": "Enables users at connected institutions to monitor Internet services in real time." + }, + "netflow": { + "name": "Netflow tool", + "category": "network_services", + "description": "Network protocol for collecting IP traffic information and monitoring network traffic." + }, + "managed-router": { + "name": "Managed router service", + "category": "network_services", + "description": "Remote network router support to institutions." + }, + "intrusion": { + "name": "Intrusion detection", + "category": "security", + "description": "Systems for detecting and preventing Intrusions (IDS/IPS)." + }, + "security-audit": { + "name": "Security auditing", + "category": "security", + "description": "Carrying out vulnerability assesments and security reviews of user systems and resources on their behalf." + }, + "web-filtering": { + "name": "Web filtering", + "category": "security", + "description": "Centralised web content filtering service for protection against access to inappropriate content." + }, + "pgp-key": { + "name": "PGP key server", + "category": "security", + "description": "Operation of PGP key server." + }, + "identifier-reg": { + "name": "Identifier registry", + "category": "security", + "description": "Registering of unique and automatically-processable identifiers in the form of text or numeric strings." + }, + "post-production": { + "name": "Media post production", + "category": "multimedia", + "description": "Work undertaken after the process of recording (or production) e.g. editing, synchronisation." + }, + "e-portfolio": { + "name": "e-portfolio service", + "category": "collaboration", + "description": "Functions to create and share user professional and career e-portfolios." + }, + "user-conference": { + "name": "User conferences", + "category": "professional_services", + "description": "Hosting of regular user conferences." + }, + "user-portal": { + "name": "User portals", + "category": "professional_services", + "description": "User portal for service management and monitoring." + }, + "admin-tools": { + "name": "Finance/admin systems", + "category": "professional_services", + "description": "Provision of ICT systems used in finance and administration." + }, + "nameserver": { + "name": "Nameserver services", + "category": "isp_support", + "description": "Operation of nameservers and maintenance of DNS information on behalf of users." + }, + "saas": { + "name": "SaaS", + "category": "storage_and_hosting", + "description": "Software as a service e.g. GoogleApps for Education." + } +} \ No newline at end of file diff --git a/compendium_v2/routes/api.py b/compendium_v2/routes/api.py index 9af16ad2..cc7ac6e0 100644 --- a/compendium_v2/routes/api.py +++ b/compendium_v2/routes/api.py @@ -16,6 +16,7 @@ from compendium_v2.routes.nren import routes as nren_routes from compendium_v2.routes.response import routes as response_routes from compendium_v2.routes.traffic import routes as traffic_routes from compendium_v2.routes.institutions_urls import routes as institutions_urls_routes +from compendium_v2.routes.nren_services import routes as nren_services_routes routes = Blueprint('compendium-v2-api', __name__) routes.register_blueprint(budget_routes, url_prefix='/budget') @@ -31,6 +32,7 @@ routes.register_blueprint(nren_routes, url_prefix='/nren') routes.register_blueprint(response_routes, url_prefix='/response') routes.register_blueprint(traffic_routes, url_prefix='/traffic') routes.register_blueprint(institutions_urls_routes, url_prefix='/institutions-urls') +routes.register_blueprint(nren_services_routes, url_prefix='/nren-services') logger = logging.getLogger(__name__) diff --git a/compendium_v2/routes/nren_services.py b/compendium_v2/routes/nren_services.py new file mode 100644 index 00000000..fde064d8 --- /dev/null +++ b/compendium_v2/routes/nren_services.py @@ -0,0 +1,58 @@ +from typing import Any + +from flask import Blueprint, jsonify +from sqlalchemy import select + +from compendium_v2.db import db +from compendium_v2.db.presentation_models import NREN, InstitutionURLs, NRENService, Service +from compendium_v2.routes import common + +routes = Blueprint('nren-services', __name__) + +NREN_SERVICES_RESPONSE_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'nren_services': { + 'type': 'object', + 'properties': { + 'nren': {'type': 'string'}, + 'nren_country': {'type': 'string'}, + 'year': {'type': 'integer'}, + 'urls': {'type': 'array', 'items': {'type': 'string'}} + }, + 'required': ['nren', 'nren_country', 'year', 'urls'], # TODO + 'additionalProperties': False + } + }, + 'type': 'array', + 'items': {'$ref': '#/definitions/nren_services'} +} + + +@routes.route('/', methods=['GET']) +@common.require_accepts_json +def nren_service_view() -> Any: + """ + handler for /api/nren-services/ requests + Endpoint for getting the service matrix that shows which NREN uses which services. + + This endpoint retrieves the URLs of webpages that list the institutions + or organizations connected to the NREN. + Many NRENs have one or more pages on their website listing such user institutions. + + response will be formatted as per NREN_SERVICES_RESPONSE_SCHEMA + + :return: + """ + + def _extract_data(institution: InstitutionURLs) -> dict: + return { + 'nren': institution.nren.name, + 'nren_country': institution.nren.country, + 'year': institution.year, + # TODO + } + + entries = [] + # TODO + return jsonify(entries) diff --git a/test/conftest.py b/test/conftest.py index 591fe482..ff66f316 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,6 +7,7 @@ from sqlalchemy import select from flask_login import LoginManager # type: ignore import compendium_v2 from compendium_v2.db import db, presentation_models, survey_models +from compendium_v2.db.presentation_model_enums import ServiceCategory from compendium_v2.survey_db import model as survey_db_model from compendium_v2.auth.session_management import setup_login_manager, User, ROLES @@ -408,3 +409,57 @@ def test_institution_urls_data(app): created_nrens = _create_and_save_nrens(unique_nren_names) _create_and_save_institution_urls(predefined_nrens_and_years, created_nrens) db.session.commit() + + +@pytest.fixture +def test_nren_services_data(app): + def _create_and_save_nrens(nren_names): + nrens = {} + for nren_name in nren_names: + nren_instance = presentation_models.NREN(name=nren_name, country='country') + nrens[nren_name] = nren_instance + db.session.add(nren_instance) + return nrens + + def _create_and_save_services(services): + services_map = {} + for service in services: + service_instance = presentation_models.Service( + name=service[2], + name_key=service[3], + description='description', + category=service[4], + ) + services_map[service[3]] = service_instance + db.session.add(service_instance) + + return services_map + + def _create_and_save_nren_services(nren_services, nrens, services): + for nren_name, year, service_name, service_name_key, service_category in nren_services: + nren_instance = nrens[nren_name] + service_instance = services[service_name_key] + + institution_urls_model = presentation_models.NRENService( + nren=nren_instance, + year=year, + service=service_instance, + ) + + db.session.add(institution_urls_model) + + with app.app_context(): + predefined_nrens_services = [ + ('nren1', 2019, 'service1', 'service1_key', ServiceCategory.network_services.value), + ('nren1', 2020, 'service1', 'service1_key', ServiceCategory.network_services.value), + ('nren1', 2021, 'service1', 'service1_key', ServiceCategory.network_services.value), + ('nren2', 2019, 'service2', 'service2_key', ServiceCategory.professional_services.value), + ('nren2', 2021, 'service2', 'service2_key', ServiceCategory.professional_services.value) + ] + + unique_nren_names = {nren[0] for nren in predefined_nrens_services} + unique_services = {nren[1:] for nren in predefined_nrens_services} + created_nrens = _create_and_save_nrens(unique_nren_names) + created_services = _create_and_save_services(unique_services) + _create_and_save_nren_services(predefined_nrens_services, created_nrens, created_services) + db.session.commit() diff --git a/test/test_nren_services.py b/test/test_nren_services.py new file mode 100644 index 00000000..00ba6aba --- /dev/null +++ b/test/test_nren_services.py @@ -0,0 +1,14 @@ +import json +import jsonschema + +from compendium_v2.routes.institutions_urls import INSTITUTION_URLS_RESPONSE_SCHEMA + + +def test_institutions_urls_response(client, test_nren_services_data): + rv = client.get( + '/api/nren-services/', + headers={'Accept': ['application/json']}) + assert rv.status_code == 200 + result = json.loads(rv.data.decode('utf-8')) + jsonschema.validate(result, INSTITUTION_URLS_RESPONSE_SCHEMA) + assert result diff --git a/test/test_survey_publisher_legacy_excel.py b/test/test_survey_publisher_legacy_excel.py index 5a20fc06..4f23686a 100644 --- a/test/test_survey_publisher_legacy_excel.py +++ b/test/test_survey_publisher_legacy_excel.py @@ -10,7 +10,7 @@ EXCEL_FILE = os.path.join(os.path.dirname(__file__), "data", "2021_Organisation_ def test_publisher(app_with_survey_db, mocker, dummy_config): - mocker.patch('compendium_v2.publishers.excel_parser.EXCEL_FILE', EXCEL_FILE) + mocker.patch('compendium_v2.publishers.excel_parser.EXCEL_FILE_ORGANISATION', EXCEL_FILE) with app_with_survey_db.app_context(): nren_names = ['SURF', 'KIFU', 'University of Malta', 'ASNET-AM', 'SIKT', 'LAT', 'RASH', 'ANAS', 'GRNET', 'CSC'] @@ -94,3 +94,6 @@ def test_publisher(app_with_survey_db, mocker, dummy_config): assert len(asnet2021) == 1 assert asnet2021[0].organization\ == 'Institute for Informatics and Automation Problems of the National Academy of Sciences of Armenia' + + service_data = db.session.scalars(select(presentation_models.Service)).all() + assert len(service_data) == 100 \ No newline at end of file diff --git a/test/test_db_survey_publisher_2022.py b/test/test_survey_publisher_old_db_2022.py similarity index 100% rename from test/test_db_survey_publisher_2022.py rename to test/test_survey_publisher_old_db_2022.py -- GitLab From f2b9459914c59430f5ab3d03b9cc4bd134916de6 Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Tue, 19 Sep 2023 15:54:07 +0200 Subject: [PATCH 2/7] fix migration script after merging --- .../1fbc4582c0ab_added_service_and_nrenservice_tables.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py index 1c16c6a9..c3dfe8fe 100644 --- a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py +++ b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py @@ -1,7 +1,7 @@ """added Service and NRENService tables Revision ID: 1fbc4582c0ab -Revises: e17f9e6c90ab +Revises: 87e1e35051a0 Create Date: 2023-09-15 16:54:34.700341 """ @@ -11,14 +11,13 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision = '1fbc4582c0ab' -down_revision = 'e17f9e6c90ab' +down_revision = '87e1e35051a0' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').create(op.get_bind()) op.create_table('service', sa.Column('id', sa.Integer(), nullable=False), sa.Column('name_key', sa.String(length=128), nullable=False), @@ -46,5 +45,4 @@ def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_table('nren_service') op.drop_table('service') - sa.Enum('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory').drop(op.get_bind()) # ### end Alembic commands ### -- GitLab From ae70cf84b3e19b0f3b96f83c17c47af85ea04129 Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Tue, 19 Sep 2023 22:42:50 +0200 Subject: [PATCH 3/7] change datamodel a bit and apply some fixes to the publisher --- compendium_v2/db/presentation_models.py | 14 +++++-------- ...ab_added_service_and_nrenservice_tables.py | 18 ++++++++--------- .../survey_publisher_legacy_excel.py | 20 +++++++++++++------ test/test_survey_publisher_legacy_excel.py | 13 +++++++++--- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/compendium_v2/db/presentation_models.py b/compendium_v2/db/presentation_models.py index d6f02456..f5edb05a 100644 --- a/compendium_v2/db/presentation_models.py +++ b/compendium_v2/db/presentation_models.py @@ -22,13 +22,10 @@ logger = logging.getLogger(__name__) str128 = Annotated[str, 128] str128_pk = Annotated[str, mapped_column(String(128), primary_key=True)] str256_pk = Annotated[str, mapped_column(String(256), primary_key=True)] -str128_unique = Annotated[str, mapped_column(String(128), unique=True)] int_pk = Annotated[int, mapped_column(primary_key=True)] -str_text = Annotated[str, mapped_column(Text)] int_pk_fkNREN = Annotated[int, mapped_column(ForeignKey("nren.id"), primary_key=True)] user_category_pk = Annotated[UserCategory, mapped_column(primary_key=True)] json_str_list = Annotated[List[str], mapped_column(JSON)] -int_pk_fkService = Annotated[int, mapped_column(ForeignKey("service.id"), primary_key=True)] ExternalConnection = TypedDict( 'ExternalConnection', @@ -479,11 +476,10 @@ class NetworkAutomation(db.Model): class Service(db.Model): __tablename__ = 'service' - id: Mapped[int_pk] - name_key: Mapped[str128_unique] + name_key: Mapped[str128_pk] name: Mapped[str128] category: Mapped[ServiceCategory] - description: Mapped[str_text] + description: Mapped[str] class NRENService(db.Model): @@ -492,8 +488,8 @@ class NRENService(db.Model): nren_id: Mapped[int_pk_fkNREN] nren: Mapped[NREN] = relationship(lazy='joined') year: Mapped[int_pk] - service_id: Mapped[int_pk_fkService] + service_key: Mapped[str128] = mapped_column(ForeignKey("service.name_key"), primary_key=True) service: Mapped[Service] = relationship(lazy='joined') product_name: Mapped[str128] - additional_information: Mapped[str_text] - official_description: Mapped[str_text] + additional_information: Mapped[str] + official_description: Mapped[str] diff --git a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py index c3dfe8fe..7bcc398e 100644 --- a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py +++ b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py @@ -2,7 +2,7 @@ Revision ID: 1fbc4582c0ab Revises: 87e1e35051a0 -Create Date: 2023-09-15 16:54:34.700341 +Create Date: 2023-09-19 22:23:36.448210 """ from alembic import op @@ -19,24 +19,22 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### op.create_table('service', - sa.Column('id', sa.Integer(), nullable=False), sa.Column('name_key', sa.String(length=128), nullable=False), sa.Column('name', sa.String(), nullable=False), sa.Column('category', postgresql.ENUM('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory', create_type=False), nullable=False), - sa.Column('description', sa.Text(), nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('pk_service')), - sa.UniqueConstraint('name_key', name=op.f('uq_service_name_key')) + sa.Column('description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('name_key', name=op.f('pk_service')) ) op.create_table('nren_service', sa.Column('nren_id', sa.Integer(), nullable=False), sa.Column('year', sa.Integer(), nullable=False), - sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('service_key', sa.String(length=128), nullable=False), sa.Column('product_name', sa.String(), nullable=False), - sa.Column('additional_information', sa.Text(), nullable=False), - sa.Column('official_description', sa.Text(), nullable=False), + sa.Column('additional_information', sa.String(), nullable=False), + sa.Column('official_description', sa.String(), nullable=False), sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_nren_service_nren_id_nren')), - sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_nren_service_service_id_service')), - sa.PrimaryKeyConstraint('nren_id', 'year', 'service_id', name=op.f('pk_nren_service')) + sa.ForeignKeyConstraint(['service_key'], ['service.name_key'], name=op.f('fk_nren_service_service_key_service')), + sa.PrimaryKeyConstraint('nren_id', 'year', 'service_key', name=op.f('pk_nren_service')) ) # ### end Alembic commands ### diff --git a/compendium_v2/publishers/survey_publisher_legacy_excel.py b/compendium_v2/publishers/survey_publisher_legacy_excel.py index 8f41b196..dfb6b6b5 100644 --- a/compendium_v2/publishers/survey_publisher_legacy_excel.py +++ b/compendium_v2/publishers/survey_publisher_legacy_excel.py @@ -242,15 +242,23 @@ def db_services_migration(): def db_nren_services_migration(nren_dict): + services = [s for s in db.session.scalars(select(presentation_models.Service))] + for service_info in excel_parser.fetch_nren_services_excel_data(): - service = db.session.query(presentation_models.Service).filter_by( - name_key=service_info['service_name_key']).first() + [service] = [s for s in services if s.name_key == service_info['service_name_key']] + + abbrev = service_info['nren_name'] + if abbrev not in nren_dict: + logger.warning(f'{abbrev} unknown. Skipping.') + continue + nren = nren_dict[abbrev] nren_service = presentation_models.NRENService( - nren=nren_dict[service_info['nren_name']], - nren_id=nren_dict[service_info['nren_name']].id, + nren=nren, + nren_id=nren.id, year=service_info['year'], service=service, + service_key=service.name_key, product_name=service_info['product_name'], additional_information=service_info['additional_information'], official_description=service_info['official_description'] @@ -261,7 +269,7 @@ def db_nren_services_migration(nren_dict): db.session.commit() -def _cli(config, app): +def _cli(app): with app.app_context(): nren_dict = helpers.get_uppercase_nren_dict() db_budget_migration(nren_dict) @@ -284,7 +292,7 @@ def cli(config): app = compendium_v2._create_app_with_db(app_config) print("survey-publisher-v1 starting") - _cli(app_config, app) + _cli(app) if __name__ == "__main__": diff --git a/test/test_survey_publisher_legacy_excel.py b/test/test_survey_publisher_legacy_excel.py index 4f23686a..b066767e 100644 --- a/test/test_survey_publisher_legacy_excel.py +++ b/test/test_survey_publisher_legacy_excel.py @@ -9,7 +9,7 @@ from compendium_v2.publishers.survey_publisher_legacy_excel import _cli EXCEL_FILE = os.path.join(os.path.dirname(__file__), "data", "2021_Organisation_DataSeries.xlsx") -def test_publisher(app_with_survey_db, mocker, dummy_config): +def test_excel_publisher(app_with_survey_db, mocker): mocker.patch('compendium_v2.publishers.excel_parser.EXCEL_FILE_ORGANISATION', EXCEL_FILE) with app_with_survey_db.app_context(): @@ -17,7 +17,7 @@ def test_publisher(app_with_survey_db, mocker, dummy_config): db.session.add_all([presentation_models.NREN(name=nren_name, country='country') for nren_name in nren_names]) db.session.commit() - _cli(dummy_config, app_with_survey_db) + _cli(app_with_survey_db) with app_with_survey_db.app_context(): budget_count = db.session.scalar(select(func.count(presentation_models.BudgetEntry.year))) @@ -96,4 +96,11 @@ def test_publisher(app_with_survey_db, mocker, dummy_config): == 'Institute for Informatics and Automation Problems of the National Academy of Sciences of Armenia' service_data = db.session.scalars(select(presentation_models.Service)).all() - assert len(service_data) == 100 \ No newline at end of file + assert len(service_data) == 74 + + nren_service_data = db.session.scalars(select(presentation_models.NRENService)).all() + # test a random entry + sikt2022 = [x for x in nren_service_data + if x.nren.name == 'SIKT' and x.year == 2022 and x.service.name == 'Journal access'] + assert len(sikt2022) == 1 + assert sikt2022[0].additional_information.startswith("Sikt negotiates license a") -- GitLab From c910ab456a1e3c0656788dff1a08a003897a6a2a Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Tue, 19 Sep 2023 23:18:34 +0200 Subject: [PATCH 4/7] fix tox issues --- compendium_v2/db/presentation_models.py | 2 +- ...ab_added_service_and_nrenservice_tables.py | 41 +++++++++++-------- compendium_v2/publishers/excel_parser.py | 3 +- compendium_v2/routes/nren_services.py | 23 +++++------ test/test_nren_services.py | 4 +- 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/compendium_v2/db/presentation_models.py b/compendium_v2/db/presentation_models.py index f5edb05a..a25200e7 100644 --- a/compendium_v2/db/presentation_models.py +++ b/compendium_v2/db/presentation_models.py @@ -6,7 +6,7 @@ from decimal import Decimal from typing import List, Optional from typing_extensions import Annotated, TypedDict -from sqlalchemy import String, JSON, Text +from sqlalchemy import String, JSON from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.schema import ForeignKey diff --git a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py index 7bcc398e..1c3dc396 100644 --- a/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py +++ b/compendium_v2/migrations/versions/1fbc4582c0ab_added_service_and_nrenservice_tables.py @@ -18,23 +18,32 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('service', - sa.Column('name_key', sa.String(length=128), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('category', postgresql.ENUM('network_services', 'isp_support', 'security', 'identity', 'collaboration', 'multimedia', 'storage_and_hosting', 'professional_services', name='servicecategory', create_type=False), nullable=False), - sa.Column('description', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('name_key', name=op.f('pk_service')) + op.create_table( + 'service', + sa.Column('name_key', sa.String(length=128), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('category', postgresql.ENUM( + 'network_services', 'isp_support', 'security', 'identity', + 'collaboration', 'multimedia', 'storage_and_hosting', + 'professional_services', name='servicecategory', + create_type=False), nullable=False + ), + sa.Column('description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('name_key', name=op.f('pk_service')) ) - op.create_table('nren_service', - sa.Column('nren_id', sa.Integer(), nullable=False), - sa.Column('year', sa.Integer(), nullable=False), - sa.Column('service_key', sa.String(length=128), nullable=False), - sa.Column('product_name', sa.String(), nullable=False), - sa.Column('additional_information', sa.String(), nullable=False), - sa.Column('official_description', sa.String(), nullable=False), - sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_nren_service_nren_id_nren')), - sa.ForeignKeyConstraint(['service_key'], ['service.name_key'], name=op.f('fk_nren_service_service_key_service')), - sa.PrimaryKeyConstraint('nren_id', 'year', 'service_key', name=op.f('pk_nren_service')) + op.create_table( + 'nren_service', + sa.Column('nren_id', sa.Integer(), nullable=False), + sa.Column('year', sa.Integer(), nullable=False), + sa.Column('service_key', sa.String(length=128), nullable=False), + sa.Column('product_name', sa.String(), nullable=False), + sa.Column('additional_information', sa.String(), nullable=False), + sa.Column('official_description', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['nren_id'], ['nren.id'], name=op.f('fk_nren_service_nren_id_nren')), + sa.ForeignKeyConstraint( + ['service_key'], ['service.name_key'], name=op.f('fk_nren_service_service_key_service') + ), + sa.PrimaryKeyConstraint('nren_id', 'year', 'service_key', name=op.f('pk_nren_service')) ) # ### end Alembic commands ### diff --git a/compendium_v2/publishers/excel_parser.py b/compendium_v2/publishers/excel_parser.py index b842950b..ce1efff4 100644 --- a/compendium_v2/publishers/excel_parser.py +++ b/compendium_v2/publishers/excel_parser.py @@ -1,5 +1,4 @@ import logging -from typing import Dict import openpyxl @@ -398,7 +397,7 @@ def fetch_traffic_excel_data(): yield from create_points_for_year(2022, 2) -def fetch_nren_services_excel_data() -> Dict[str, str]: +def fetch_nren_services_excel_data(): wb = openpyxl.load_workbook(EXCEL_FILE_NREN_SERVICES, data_only=True, read_only=True) ws = wb["Sheet1"] rows = list(ws.rows) diff --git a/compendium_v2/routes/nren_services.py b/compendium_v2/routes/nren_services.py index fde064d8..25c58aaf 100644 --- a/compendium_v2/routes/nren_services.py +++ b/compendium_v2/routes/nren_services.py @@ -1,10 +1,10 @@ from typing import Any from flask import Blueprint, jsonify -from sqlalchemy import select +# from sqlalchemy import select -from compendium_v2.db import db -from compendium_v2.db.presentation_models import NREN, InstitutionURLs, NRENService, Service +# from compendium_v2.db import db +# from compendium_v2.db.presentation_models import NREN, InstitutionURLs, NRENService, Service from compendium_v2.routes import common routes = Blueprint('nren-services', __name__) @@ -45,14 +45,13 @@ def nren_service_view() -> Any: :return: """ - def _extract_data(institution: InstitutionURLs) -> dict: - return { - 'nren': institution.nren.name, - 'nren_country': institution.nren.country, - 'year': institution.year, - # TODO - } + # def _extract_data(institution: InstitutionURLs) -> dict: + # return { + # 'nren': institution.nren.name, + # 'nren_country': institution.nren.country, + # 'year': institution.year, + # # TODO + # } - entries = [] # TODO - return jsonify(entries) + return jsonify([]) diff --git a/test/test_nren_services.py b/test/test_nren_services.py index 00ba6aba..2e778b4e 100644 --- a/test/test_nren_services.py +++ b/test/test_nren_services.py @@ -1,10 +1,12 @@ import json import jsonschema +import pytest from compendium_v2.routes.institutions_urls import INSTITUTION_URLS_RESPONSE_SCHEMA -def test_institutions_urls_response(client, test_nren_services_data): +@pytest.mark.skip(reason="wip") +def test_nren_services(client, test_nren_services_data): rv = client.get( '/api/nren-services/', headers={'Accept': ['application/json']}) -- GitLab From 01a5560a0670ca7b64f9ed1bdb461cfb29d11ed8 Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Wed, 20 Sep 2023 15:45:32 +0200 Subject: [PATCH 5/7] complete nren services api endpoint --- compendium_v2/routes/institutions_urls.py | 12 +++--- compendium_v2/routes/nren_services.py | 45 ++++++++++++++--------- test/conftest.py | 13 ++++--- test/test_nren_services.py | 6 +-- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/compendium_v2/routes/institutions_urls.py b/compendium_v2/routes/institutions_urls.py index 8af1bbb6..45c71287 100644 --- a/compendium_v2/routes/institutions_urls.py +++ b/compendium_v2/routes/institutions_urls.py @@ -1,10 +1,8 @@ from typing import Any from flask import Blueprint, jsonify -from sqlalchemy import select -from compendium_v2.db import db -from compendium_v2.db.presentation_models import NREN, InstitutionURLs +from compendium_v2.db.presentation_models import InstitutionURLs from compendium_v2.routes import common routes = Blueprint('institutions-urls', __name__) @@ -40,7 +38,10 @@ def institutions_urls_view() -> Any: or organizations connected to the NREN. Many NRENs have one or more pages on their website listing such user institutions. - response will be formatted as per INSTITUTION_URLS_RESPONSE_SCHEMA + response will be formatted as: + + .. asjson:: + compendium_v2.routes.institutions_urls.INSTITUTION_URLS_RESPONSE_SCHEMA :return: """ @@ -54,8 +55,7 @@ def institutions_urls_view() -> Any: } entries = [] - records = db.session.scalars( - select(InstitutionURLs).join(NREN).order_by(NREN.name.asc(), InstitutionURLs.year.desc())) + records = common.get_data(InstitutionURLs) for entry in records: entries.append(_extract_data(entry)) diff --git a/compendium_v2/routes/nren_services.py b/compendium_v2/routes/nren_services.py index 25c58aaf..f4ed08b9 100644 --- a/compendium_v2/routes/nren_services.py +++ b/compendium_v2/routes/nren_services.py @@ -1,10 +1,8 @@ from typing import Any from flask import Blueprint, jsonify -# from sqlalchemy import select -# from compendium_v2.db import db -# from compendium_v2.db.presentation_models import NREN, InstitutionURLs, NRENService, Service +from compendium_v2.db.presentation_models import NRENService from compendium_v2.routes import common routes = Blueprint('nren-services', __name__) @@ -18,9 +16,15 @@ NREN_SERVICES_RESPONSE_SCHEMA = { 'nren': {'type': 'string'}, 'nren_country': {'type': 'string'}, 'year': {'type': 'integer'}, - 'urls': {'type': 'array', 'items': {'type': 'string'}} + 'product_name': {'type': 'string'}, + 'additional_information': {'type': 'string'}, + 'official_description': {'type': 'string'}, + 'service_name': {'type': 'string'}, + 'service_category': {'type': 'string'}, + 'service_description': {'type': 'string'} }, - 'required': ['nren', 'nren_country', 'year', 'urls'], # TODO + 'required': ['nren', 'nren_country', 'year', 'product_name', 'additional_information', + 'official_description', 'service_name', 'service_category', 'service_description'], 'additionalProperties': False } }, @@ -36,22 +40,27 @@ def nren_service_view() -> Any: handler for /api/nren-services/ requests Endpoint for getting the service matrix that shows which NREN uses which services. - This endpoint retrieves the URLs of webpages that list the institutions - or organizations connected to the NREN. - Many NRENs have one or more pages on their website listing such user institutions. + response will be formatted as: - response will be formatted as per NREN_SERVICES_RESPONSE_SCHEMA + .. asjson:: + compendium_v2.routes.nren_services.NREN_SERVICES_RESPONSE_SCHEMA :return: """ - # def _extract_data(institution: InstitutionURLs) -> dict: - # return { - # 'nren': institution.nren.name, - # 'nren_country': institution.nren.country, - # 'year': institution.year, - # # TODO - # } + def _extract_data(nren_service: NRENService) -> dict: + return { + 'nren': nren_service.nren.name, + 'nren_country': nren_service.nren.country, + 'year': nren_service.year, + 'product_name': nren_service.product_name, + 'additional_information': nren_service.additional_information, + 'official_description': nren_service.official_description, + 'service_name': nren_service.service.name, + 'service_category': nren_service.service.category.value, + 'service_description': nren_service.service.description + } + + entries = [_extract_data(entry) for entry in common.get_data(NRENService)] - # TODO - return jsonify([]) + return jsonify(entries) diff --git a/test/conftest.py b/test/conftest.py index ff66f316..7bb203ae 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -425,12 +425,12 @@ def test_nren_services_data(app): services_map = {} for service in services: service_instance = presentation_models.Service( - name=service[2], - name_key=service[3], + name=service[0], + name_key=service[1], description='description', - category=service[4], + category=service[2], ) - services_map[service[3]] = service_instance + services_map[service[1]] = service_instance db.session.add(service_instance) return services_map @@ -444,6 +444,9 @@ def test_nren_services_data(app): nren=nren_instance, year=year, service=service_instance, + product_name="brand name", + additional_information="extra info", + official_description="description" ) db.session.add(institution_urls_model) @@ -458,7 +461,7 @@ def test_nren_services_data(app): ] unique_nren_names = {nren[0] for nren in predefined_nrens_services} - unique_services = {nren[1:] for nren in predefined_nrens_services} + unique_services = {nren[2:] for nren in predefined_nrens_services} created_nrens = _create_and_save_nrens(unique_nren_names) created_services = _create_and_save_services(unique_services) _create_and_save_nren_services(predefined_nrens_services, created_nrens, created_services) diff --git a/test/test_nren_services.py b/test/test_nren_services.py index 2e778b4e..ce832e6e 100644 --- a/test/test_nren_services.py +++ b/test/test_nren_services.py @@ -1,16 +1,14 @@ import json import jsonschema -import pytest -from compendium_v2.routes.institutions_urls import INSTITUTION_URLS_RESPONSE_SCHEMA +from compendium_v2.routes.nren_services import NREN_SERVICES_RESPONSE_SCHEMA -@pytest.mark.skip(reason="wip") def test_nren_services(client, test_nren_services_data): rv = client.get( '/api/nren-services/', headers={'Accept': ['application/json']}) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, INSTITUTION_URLS_RESPONSE_SCHEMA) + jsonschema.validate(result, NREN_SERVICES_RESPONSE_SCHEMA) assert result -- GitLab From 44dd5fdc8456c9189b5199a0e1244edf7f789f65 Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Wed, 20 Sep 2023 16:34:15 +0200 Subject: [PATCH 6/7] survey publisher (v2) for the service matrix --- compendium_v2/publishers/survey_publisher.py | 20 ++++++++++++++++++-- test/test_survey_publisher.py | 12 ++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/compendium_v2/publishers/survey_publisher.py b/compendium_v2/publishers/survey_publisher.py index 10a50baf..ccd4b094 100644 --- a/compendium_v2/publishers/survey_publisher.py +++ b/compendium_v2/publishers/survey_publisher.py @@ -23,7 +23,7 @@ from compendium_v2.db.presentation_models import BudgetEntry, ChargingStructure, CommercialChargingLevel, RemoteCampuses, DarkFibreLease, DarkFibreInstalled, FibreLight, \ NetworkMapUrls, MonitoringTools, PassiveMonitoring, TrafficStatistics, SiemVendors, \ CertificateProviders, WeatherMap, PertTeam, AlienWave, Capacity, NonREPeers, TrafficRatio, \ - OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation + OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation, NRENService from compendium_v2.db.presentation_model_enums import CarryMechanism, CommercialCharges, YesNoPlanned, \ CommercialConnectivityCoverage, ConnectivityCoverage, FeeType, MonitoringMethod, ServiceCategory, UserCategory from compendium_v2.db.survey_models import ResponseStatus, SurveyResponse @@ -64,7 +64,7 @@ def _map_2023(nren, answers) -> None: CommercialChargingLevel, RemoteCampuses, DarkFibreLease, DarkFibreInstalled, FibreLight, NetworkMapUrls, MonitoringTools, PassiveMonitoring, TrafficStatistics, SiemVendors, CertificateProviders, WeatherMap, PertTeam, AlienWave, Capacity, NonREPeers, TrafficRatio, - OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation]: + OpsAutomation, NetworkFunctionVirtualisation, NetworkAutomation, NRENService]: db.session.execute(delete(table_class).where(table_class.year == year)) # type: ignore answers = answers["data"] @@ -516,6 +516,22 @@ def _map_2023(nren, answers) -> None: network_automation_specifics=network_automation_tasks )) + all_services = {} + for question_name in ["services_collaboration", "services_hosting", "services_identity", "services_isp", + "services_multimedia", "services_network", "services_professional", "services_security"]: + all_services.update(answers.get(question_name, {})) + + for service_key, answers in all_services.items(): + offered = answers.get("offered") + if offered == ["yes"]: + db.session.add(NRENService( + nren_id=nren.id, nren=nren, year=year, + service_key=service_key, + product_name=answers.get("name", ""), + additional_information=answers.get("additional_information", ""), + official_description=answers.get("description", "") + )) + def publish(year): responses = db.session.scalars( diff --git a/test/test_survey_publisher.py b/test/test_survey_publisher.py index 3e66d6f0..2889eebb 100644 --- a/test/test_survey_publisher.py +++ b/test/test_survey_publisher.py @@ -306,3 +306,15 @@ def test_v2_publisher_full(app): assert network_automation.network_automation_specifics == [ "config_management", "provisioning", "data_collection", "compliance", "reporting", "troubleshooting" ] + + services = [s for s in db.session.scalars(select(presentation_models.NRENService))] + assert len(services) == 59 + virtual_learning_environment = [s for s in services if s.service_key == "virtual-learning-environment"] + assert len(virtual_learning_environment) == 1 + assert virtual_learning_environment[0].additional_information == "zd" + csirt = [s for s in services if s.service_key == "csirt"] + assert len(csirt) == 1 + assert csirt[0].official_description == "zdf" + connectivity = [s for s in services if s.service_key == "connectivity"] + assert len(connectivity) == 1 + assert connectivity[0].product_name == "zfz" -- GitLab From 453cfeb5b323748364b3b9312a5b561550d695ad Mon Sep 17 00:00:00 2001 From: Remco Tukker <remco.tukker@geant.org> Date: Wed, 20 Sep 2023 16:41:50 +0200 Subject: [PATCH 7/7] ensure the required data files are part of the package --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 58874ea5..19e22890 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,5 @@ include compendium_v2/templates/survey-index.html include compendium_v2/migrations/alembic.ini recursive-include compendium_v2/migrations/versions * recursive-include compendium_v2/migrations/surveymodels * -recursive-include compendium_v2/background_task/resources * +recursive-include compendium_v2/resources * recursive-exclude test * -- GitLab