diff --git a/app.py b/app.py index 100b6aa2..3f198e60 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,7 @@ from resources.LoginWithGithub import namespace_login_with_github from resources.Map import namespace_map from resources.Match import namespace_match +from resources.Metrics import namespace_metrics from resources.Notifications import namespace_notifications from resources.OAuth import namespace_oauth from resources.Permissions import namespace_permissions @@ -70,6 +71,7 @@ namespace_bulk, namespace_match, namespace_locations, + namespace_metrics, ] MY_PREFIX = "/api" diff --git a/database_client/database_client.py b/database_client/database_client.py index 131d4d52..8a42d422 100644 --- a/database_client/database_client.py +++ b/database_client/database_client.py @@ -1486,3 +1486,48 @@ def get_location_by_id(self, location_id: int): where_mappings={"id": location_id}, alias_mappings={"id": "location_id"}, ) + + def get_metrics(self): + result = self.execute_raw_sql( + """ + SELECT + COUNT(*), + 'source_count' "Count Type" + FROM + DATA_SOURCES + UNION + SELECT + COUNT(DISTINCT (AGENCY_ID)), + 'agency_count' "Count Type" + FROM + LINK_AGENCIES_DATA_SOURCES + UNION + SELECT + COUNT(DISTINCT L.ID), + 'state_count' "Count Type" + FROM + LINK_AGENCIES_DATA_SOURCES LINK + INNER JOIN AGENCIES A ON A.ID = LINK.AGENCY_ID + JOIN DEPENDENT_LOCATIONS DL ON A.LOCATION_ID = DL.DEPENDENT_LOCATION_ID + JOIN LOCATIONS L ON L.ID = A.LOCATION_ID + OR L.ID = DL.PARENT_LOCATION_ID + WHERE + L.TYPE = 'State' + UNION + SELECT + COUNT(DISTINCT L.ID), + 'county_count' "Count Type" + FROM + LINK_AGENCIES_DATA_SOURCES LINK + INNER JOIN AGENCIES A ON A.ID = LINK.AGENCY_ID + JOIN DEPENDENT_LOCATIONS DL ON A.LOCATION_ID = DL.DEPENDENT_LOCATION_ID + JOIN LOCATIONS L ON L.ID = A.LOCATION_ID + OR L.ID = DL.PARENT_LOCATION_ID + WHERE + L.TYPE = 'County' + """ + ) + d = {} + for row in result: + d[row["Count Type"]] = row["count"] + return d diff --git a/middleware/SimpleJWT.py b/middleware/SimpleJWT.py index dc271de5..987a5ec4 100644 --- a/middleware/SimpleJWT.py +++ b/middleware/SimpleJWT.py @@ -4,7 +4,7 @@ from typing import Union import jwt -from jwt import ExpiredSignatureError, InvalidSignatureError +from jwt import InvalidSignatureError from middleware.flask_response_manager import FlaskResponseManager from middleware.util import get_env_variable diff --git a/middleware/primary_resource_logic/metrics_logic.py b/middleware/primary_resource_logic/metrics_logic.py new file mode 100644 index 00000000..0c67d1af --- /dev/null +++ b/middleware/primary_resource_logic/metrics_logic.py @@ -0,0 +1,5 @@ +from database_client.database_client import DatabaseClient + + +def get_metrics(db_client: DatabaseClient): + return db_client.get_metrics() diff --git a/middleware/schema_and_dto_logic/primary_resource_schemas/metrics_schemas.py b/middleware/schema_and_dto_logic/primary_resource_schemas/metrics_schemas.py new file mode 100644 index 00000000..b963a1c4 --- /dev/null +++ b/middleware/schema_and_dto_logic/primary_resource_schemas/metrics_schemas.py @@ -0,0 +1,10 @@ +from marshmallow import Schema, fields + +from middleware.schema_and_dto_logic.util import get_json_metadata + + +class MetricsGetResponseSchema(Schema): + source_count = fields.Int(metadata=get_json_metadata("The number of data sources")) + agency_count = fields.Int(metadata=get_json_metadata("The number of agencies")) + county_count = fields.Int(metadata=get_json_metadata("The number of counties")) + state_count = fields.Int(metadata=get_json_metadata("The number of states")) diff --git a/resources/Metrics.py b/resources/Metrics.py new file mode 100644 index 00000000..4f01ba24 --- /dev/null +++ b/resources/Metrics.py @@ -0,0 +1,29 @@ +from flask import Response + +from middleware.access_logic import AccessInfoPrimary, GET_AUTH_INFO +from middleware.decorators import endpoint_info +from middleware.primary_resource_logic.metrics_logic import get_metrics +from resources.PsycopgResource import PsycopgResource +from resources.endpoint_schema_config import SchemaConfigs +from resources.resource_helpers import ResponseInfo +from utilities.namespace import AppNamespaces, create_namespace + +namespace_metrics = create_namespace(AppNamespaces.METRICS) + + +@namespace_metrics.route("") +class Metrics(PsycopgResource): + + @endpoint_info( + namespace=namespace_metrics, + auth_info=GET_AUTH_INFO, + schema_config=SchemaConfigs.METRICS_GET, + description="Returns the metrics for the application.", + response_info=ResponseInfo( + success_message="Returns the metrics for the application." + ), + ) + def get(self, access_info: AccessInfoPrimary) -> Response: + return self.run_endpoint( + wrapper_function=get_metrics, + ) diff --git a/resources/endpoint_schema_config.py b/resources/endpoint_schema_config.py index cd49f09e..e5cffe7a 100644 --- a/resources/endpoint_schema_config.py +++ b/resources/endpoint_schema_config.py @@ -41,6 +41,9 @@ AgencyMatchSchema, MatchAgencyResponseSchema, ) +from middleware.schema_and_dto_logic.primary_resource_schemas.metrics_schemas import ( + MetricsGetResponseSchema, +) from middleware.schema_and_dto_logic.primary_resource_schemas.reset_token_schemas import ( ResetPasswordSchema, ) @@ -548,3 +551,10 @@ class SchemaConfigs(Enum): primary_output_schema=GetManyDataRequestsResponseSchema(), ) # endregion + + # region Metrics + METRICS_GET = EndpointSchemaConfig( + primary_output_schema=MetricsGetResponseSchema(), + ) + + # endregion diff --git a/tests/helper_scripts/helper_classes/RequestValidator.py b/tests/helper_scripts/helper_classes/RequestValidator.py index d0455877..cfccb48d 100644 --- a/tests/helper_scripts/helper_classes/RequestValidator.py +++ b/tests/helper_scripts/helper_classes/RequestValidator.py @@ -610,4 +610,14 @@ def get_location_related_data_requests( expected_schema=SchemaConfigs.LOCATIONS_RELATED_DATA_REQUESTS_GET.value.primary_output_schema, ) + def get_metrics( + self, + headers: dict, + ): + return self.get( + endpoint=f"/api/metrics", + headers=headers, + expected_schema=SchemaConfigs.METRICS_GET.value.primary_output_schema, + ) + # endregion diff --git a/tests/integration/test_metrics.py b/tests/integration/test_metrics.py new file mode 100644 index 00000000..df3bcf8f --- /dev/null +++ b/tests/integration/test_metrics.py @@ -0,0 +1,17 @@ +from tests.helper_scripts.helper_classes.TestDataCreatorFlask import ( + TestDataCreatorFlask, +) +from conftest import test_data_creator_flask, monkeysession + + +def test_metrics(test_data_creator_flask: TestDataCreatorFlask): + + tdc = test_data_creator_flask + metrics = tdc.request_validator.get_metrics( + headers=tdc.get_admin_tus().jwt_authorization_header + ) + + assert metrics["source_count"] > 0 + assert metrics["agency_count"] > 0 + assert metrics["county_count"] > 0 + assert metrics["state_count"] > 0 diff --git a/utilities/namespace.py b/utilities/namespace.py index 0608af6f..2bb20689 100644 --- a/utilities/namespace.py +++ b/utilities/namespace.py @@ -32,6 +32,7 @@ class AppNamespaces(Enum): BULK = NamespaceAttributes(path="bulk", description="Bulk Namespace") MATCH = NamespaceAttributes(path="match", description="Match Namespace") LOCATIONS = NamespaceAttributes(path="locations", description="Locations Namespace") + METRICS = NamespaceAttributes(path="metrics", description="Metrics Namespace") def create_namespace(