marcus67/little_brother

View on GitHub
little_brother/api/api_view_handler.py

Summary

Maintainability
B
5 hrs
Test Coverage
# -*- coding: utf-8 -*-

# Copyright (C) 2019  Marcus Rickert
#
# See https://github.com/marcus67/little_brother
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import datetime
import json

import flask

import little_brother
from little_brother import constants
from little_brother import dependency_injection
from little_brother.admin_data_handler import AdminDataHandler
from little_brother.api.master_connector import MasterConnector
from little_brother.app_control import AppControl
from little_brother.event_handler import EventHandler
from little_brother.persistence.persistent_daily_user_status import DailyUserStatus
from little_brother.persistence.persistent_dependency_injection_mix_in import PersistenceDependencyInjectionMixIn
from little_brother.persistence.persistent_user import User
from little_brother.persistence.session_context import SessionContext
from little_brother.process_handler_manager import ProcessHandlerManager
from little_brother.rule_handler import RuleHandler
from little_brother.user_manager import UserManager
from python_base_app import log_handling
from python_base_app import tools
from some_flask_helpers import blueprint_adapter

MIME_TYPE_APPLICATION_JSON = 'application/json'

API_BLUEPRINT_NAME = "API"
API_BLUEPRINT_ADAPTER = blueprint_adapter.BlueprintAdapter()

# Dummy function to trigger extraction by pybabel...
_ = lambda x: x

# ToDo: Derive ApiViewHandler from BaseViewHandler!
class ApiViewHandler(PersistenceDependencyInjectionMixIn):

    def __init__(self, p_app):

        super().__init__()

        self._appcontrol = None
        self._master_connector = None
        self._event_handler = None
        self._user_manager = None
        self._rule_handler = None
        self._admin_data_handler = None
        self._process_handler_manager = None
        self._logger = log_handling.get_logger(self.__class__.__name__)

        self._blueprint = flask.Blueprint(API_BLUEPRINT_NAME, little_brother.__name__)
        API_BLUEPRINT_ADAPTER.assign_view_handler_instance(p_blueprint=self._blueprint,
                                                           p_view_handler_instance=self)
        API_BLUEPRINT_ADAPTER.check_view_methods()
        p_app.register_blueprint(self._blueprint)

    @property
    def blueprint(self):
        return self._blueprint

    @property
    def app_control(self) -> AppControl:
        
        if self._appcontrol is None:
            self._appcontrol = dependency_injection.container[AppControl]
            
        return self._appcontrol

    @property
    def master_connector(self) -> MasterConnector:

        if self._master_connector is None:
            self._master_connector = dependency_injection.container[MasterConnector]

        return self._master_connector

    @property
    def event_handler(self) -> EventHandler:

        if self._event_handler is None:
            self._event_handler = dependency_injection.container[EventHandler]

        return self._event_handler

    @property
    def user_manager(self) -> UserManager:

        if self._user_manager is None:
            self._user_manager = dependency_injection.container[UserManager]

        return self._user_manager

    @property
    def rule_handler(self) -> RuleHandler:

        if self._rule_handler is None:
            self._rule_handler = dependency_injection.container[RuleHandler]

        return self._rule_handler

    @property
    def admin_data_handler(self) -> AdminDataHandler:

        if self._admin_data_handler is None:
            self._admin_data_handler = dependency_injection.container[AdminDataHandler]

        return self._admin_data_handler

    @property
    def processs_handler_manager(self) -> ProcessHandlerManager:
        if self._process_handler_manager is None:
            self._process_handler_manager = dependency_injection.container[ProcessHandlerManager]

        return self._process_handler_manager


    def measure(self, p_hostname, p_service, p_duration):

        self.app_control.set_prometheus_http_requests_summary(p_hostname=p_hostname,
                                                              p_service=p_service,
                                                              p_duration=p_duration)

    def api_error(self, p_message, p_status_code=constants.HTTP_STATUS_CODE_NOT_FOUND):
        msg_format = '{{ "{errortag}" : "{msg}" }}'
        msg = msg_format.format(errortag=constants.JSON_ERROR, msg=p_message)
        return flask.Response(msg, status=p_status_code, mimetype=MIME_TYPE_APPLICATION_JSON)

    def missing_parameter_error(self, p_parameter_name):
        msg_format = _("parameter '{parameter_name}' not specified")
        msg = msg_format.format(parameter_name=p_parameter_name)
        return self.api_error(p_message=msg)

    def wrong_parameter_format_error(self, p_parameter_name, p_value):
        msg_format = _("parameter '{parameter_name}' with value '{value}' has wrong format")
        msg = msg_format.format(parameter_name=p_parameter_name, value=p_value)
        return self.api_error(p_message=msg)

    def user_does_not_exist_error(self, p_username):
        msg_format = _("username '{username}' does not exist or is not being monitored")
        msg = msg_format.format(username=p_username)
        return self.api_error(p_message=msg)

    def invalid_secret_error(self):
        return self.api_error(p_message="invalid access code",
                              p_status_code=constants.HTTP_STATUS_CODE_UNAUTHORIZED)

    @API_BLUEPRINT_ADAPTER.route_method(p_rule=constants.API_URL_EVENTS, methods=["POST"])
    def api_events(self):
        request = flask.request

        with tools.TimingContext(lambda duration:self.measure(p_hostname=request.remote_addr,
                                                         p_service=request.url_rule, p_duration=duration)):
            data = request.get_json()

            event_info = self.master_connector.receive_events(p_json_data=data)

            if event_info is None:
                return self.invalid_secret_error()

            client_stats = None

            if len(event_info) > 2:
                # new format: 3 entries including client statistics
                (hostname, json_events, json_client_stats) = event_info
                client_stats = self.app_control.receive_client_stats(p_json_data=json_client_stats)

            else:
                # old format: 2 entries without client statistics
                (hostname, json_events) = event_info

            msg = "Received {count} events from host '{hostname}'"
            self._logger.debug(msg.format(count=len(json_events), hostname=hostname))

            self.app_control.update_client_info(p_hostname=hostname, p_client_stats=client_stats)
            self.app_control.queue_broadcast_event_start_master()
            self.event_handler.receive_events(p_json_data=json_events)

            return_events = self.event_handler.get_return_events(p_hostname=hostname)
            msg = "Sending {count} events back to host '{hostname}'"
            self._logger.debug(msg.format(count=len(return_events), hostname=hostname))

            return flask.Response(json.dumps(return_events, cls=tools.ObjectEncoder), status=constants.HTTP_STATUS_CODE_OK,
                                                  mimetype=MIME_TYPE_APPLICATION_JSON)


    @API_BLUEPRINT_ADAPTER.route_method(p_rule=constants.API_URL_STATUS, methods=["GET"])
    def api_status(self):
        request = flask.request

        with tools.TimingContext(lambda duration:self.measure(p_hostname=request.remote_addr,
                                                         p_service=request.url_rule, p_duration=duration)):
            username = request.args.get(constants.API_URL_PARAM_USERNAME)

            if username is None:
                return self.missing_parameter_error(p_parameter_name=constants.API_URL_PARAM_USERNAME)

            with SessionContext(p_persistence=self.persistence) as session_context:
                user_status = self.user_manager.get_current_user_status(
                    p_session_context=session_context, p_username=username)

                if user_status is None or not user_status.monitoring_active:
                    return self.user_does_not_exist_error(p_username=username)

                user = self.user_entity_manager.get_by_username(p_session_context=session_context, p_username=username)

                user_status.optional_time_available = \
                    self.get_optional_time_available_in_minutes(p_session_context=session_context, p_user=user)

                user_status.ruleset_check_interval = self.app_control._config.check_interval

                msg = "Received status request for user '{username}'"
                self._logger.debug(msg.format(username=username))

                return flask.Response(json.dumps(user_status, cls=tools.ObjectEncoder),
                                      status=constants.HTTP_STATUS_CODE_OK,
                                      mimetype=MIME_TYPE_APPLICATION_JSON)


    @API_BLUEPRINT_ADAPTER.route_method(p_rule=constants.API_URL_REQUEST_TIME_EXTENSION, methods=["POST"])
    def api_request_time_extension(self):
        request = flask.request

        with tools.TimingContext(lambda duration:self.measure(p_hostname=request.remote_addr,
                                                         p_service=request.url_rule, p_duration=duration)):
            username = request.args.get(constants.API_URL_PARAM_USERNAME)
            secret = request.args.get(constants.API_URL_PARAM_SECRET)
            extension_length_string = request.args.get(constants.API_URL_PARAM_EXTENSION_LENGTH)

            if username is None:
                return self.missing_parameter_error(p_parameter_name=constants.API_URL_PARAM_USERNAME)

            if secret is None:
                return self.missing_parameter_error(p_parameter_name=constants.API_URL_PARAM_SECRET)

            if extension_length_string is None:
                return self.missing_parameter_error(p_parameter_name=constants.API_URL_PARAM_EXTENSION_LENGTH)

            with SessionContext(p_persistence=self.persistence) as session_context:
                user = self.user_entity_manager.get_by_username(p_session_context=session_context, p_username=username)

                if user is None:
                    return  self.user_does_not_exist_error(p_username=username)

                if secret != user.access_code:
                    return self.invalid_secret_error()

                try:
                    extension_length = int(extension_length_string)

                except:
                    return self.wrong_parameter_format_error(p_parameter_name=constants.API_URL_PARAM_EXTENSION_LENGTH,
                                                             p_value=extension_length_string)

                msg = "Received time extension request for user '{username}' for {minutes} minutes"
                self._logger.debug(msg.format(username=username, minutes=extension_length))

                return self.request_time_extension(p_session_context=session_context, p_user=user,
                                                   p_time_extension_length=extension_length)

    def get_optional_time_available_in_minutes(self, p_session_context: SessionContext, p_user: User,
                                               p_reference_date: datetime.date=None):

        if p_reference_date is None:
            p_reference_date = datetime.date.today()

        user_status = self.daily_user_status_entity_manager.get_user_status(
            p_user_id=p_user.id, p_session_context=p_session_context, p_reference_date=p_reference_date)

        active_rule_set = self.rule_handler.get_active_ruleset(p_reference_date=p_reference_date,
                                                               p_rule_sets=p_user.rulesets)

        if active_rule_set.optional_time_per_day is None:
            return None

        if active_rule_set is not None:
            optional_time_per_day = int (active_rule_set.optional_time_per_day / 60)

        else:
            optional_time_per_day = 0

        if user_status is None:
            optional_time_used = 0

        else:
            optional_time_used = user_status.optional_time_used


        return optional_time_per_day - optional_time_used

    def request_time_extension(self, p_session_context: SessionContext, p_user: User,
                               p_time_extension_length: int, p_reference_date: datetime.date=None):

        optional_time_available= self.get_optional_time_available_in_minutes(p_session_context=p_session_context,
                                                                             p_user=p_user, p_reference_date=p_reference_date)

        if p_time_extension_length <= optional_time_available:

            process_infos = self.processs_handler_manager.get_process_infos()

            admin_info = self.admin_data_handler.get_admin_info(
                p_session_context=p_session_context, p_user_name=p_user.username, p_process_infos=process_infos)

            self.time_extension_entity_manager.set_time_extension_for_admin_info_and_session(
                p_session_context=p_session_context, p_admin_info=admin_info,
                p_user_name=p_user.username, p_delta=p_time_extension_length)

            session = p_session_context.get_session()

            if p_reference_date is None:
                p_reference_date = datetime.date.today()

            user_status = self.daily_user_status_entity_manager.get_user_status(
                p_user_id=p_user.id, p_session_context=p_session_context, p_reference_date=p_reference_date)

            if user_status is None:
                user_status = DailyUserStatus()
                session.add(user_status)
                user_status.reference_date = p_reference_date
                user_status.user = p_user
                user_status.optional_time_used = p_time_extension_length

            else:
                user_status.optional_time_used += p_time_extension_length

            session.commit()

            return flask.Response("OK", status=constants.HTTP_STATUS_CODE_OK, mimetype=MIME_TYPE_APPLICATION_JSON)

        else:
            return self.api_error(p_message="requested time extension too large",
                                  p_status_code=constants.HTTP_STATUS_CODE_RANGE_NOT_SATISFIABLE)

    def destroy(self):
        API_BLUEPRINT_ADAPTER.unassign_view_handler_instances()