diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a62ca0a37..4598f65842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The dev provisioning Ansible playbook now automatically generates an SSH key pair for the `flowkit` user. [#892](https://github.com/Flowminder/FlowKit/issues/892) - FlowAPI's 'joined_spatial_aggregate' endpoint now exposes unique location counts.[#949](https://github.com/Flowminder/FlowKit/issues/949) +- FlowAPI's 'joined_spatial_aggregate' endpoint now exposes subscriber degree.[#969](https://github.com/Flowminder/FlowKit/issues/969) - Flowdb now contains an auxiliary table to record outcomes of queries that can be run as part of the regular ETL process [#988](https://github.com/Flowminder/FlowKit/issues/988) ### Changed diff --git a/flowclient/flowclient/__init__.py b/flowclient/flowclient/__init__.py index 070f4bc98b..b906a861ae 100644 --- a/flowclient/flowclient/__init__.py +++ b/flowclient/flowclient/__init__.py @@ -36,6 +36,7 @@ joined_spatial_aggregate, radius_of_gyration, unique_location_counts, + subscriber_degree, ) __all__ = [ @@ -64,4 +65,5 @@ "joined_spatial_aggregate", "radius_of_gyration", "unique_location_counts", + "subscriber_degree", ] diff --git a/flowclient/flowclient/client.py b/flowclient/flowclient/client.py index cb29e3293e..3192ba5cdd 100644 --- a/flowclient/flowclient/client.py +++ b/flowclient/flowclient/client.py @@ -1285,3 +1285,39 @@ def unique_location_counts( "aggregation_unit": aggregation_unit, "subscriber_subset": subscriber_subset, } + + +def subscriber_degree( + *, + start: str, + stop: str, + direction: str = "both", + subscriber_subset: Union[dict, None] = None, +) -> dict: + """ + Return query spec for subscriber degree + + Parameters + ---------- + start : str + ISO format date of the first day of the count, e.g. "2016-01-01" + stop : str + ISO format date of the day _after_ the final date of the count, e.g. "2016-01-08" + direction : {"in", "out", "both"}, default "both" + Optionally, include only ingoing or outbound calls/texts. Can be one of "in", "out" or "both". + subscriber_subset : dict or None, default None + Subset of subscribers to include in event counts. Must be None + (= all subscribers) or a dictionary with the specification of a + subset query. + Returns + ------- + dict + Dict which functions as the query specification + """ + return { + "query_kind": "subscriber_degree", + "start": start, + "stop": stop, + "direction": direction, + "subscriber_subset": subscriber_subset, + } diff --git a/flowmachine/flowmachine/core/server/query_schemas/joined_spatial_aggregate.py b/flowmachine/flowmachine/core/server/query_schemas/joined_spatial_aggregate.py index d80784ce05..68a3b49de9 100644 --- a/flowmachine/flowmachine/core/server/query_schemas/joined_spatial_aggregate.py +++ b/flowmachine/flowmachine/core/server/query_schemas/joined_spatial_aggregate.py @@ -9,6 +9,9 @@ from flowmachine.core.server.query_schemas.radius_of_gyration import ( RadiusOfGyrationSchema, ) +from flowmachine.core.server.query_schemas.subscriber_degree import ( + SubscriberDegreeSchema, +) from flowmachine.core.server.query_schemas.unique_location_counts import ( UniqueLocationCountsSchema, ) @@ -27,6 +30,7 @@ class JoinableMetrics(OneOfSchema): type_schemas = { "radius_of_gyration": RadiusOfGyrationSchema, "unique_location_counts": UniqueLocationCountsSchema, + "subscriber_degree": SubscriberDegreeSchema, } diff --git a/flowmachine/flowmachine/core/server/query_schemas/subscriber_degree.py b/flowmachine/flowmachine/core/server/query_schemas/subscriber_degree.py new file mode 100644 index 0000000000..ec2f259b5b --- /dev/null +++ b/flowmachine/flowmachine/core/server/query_schemas/subscriber_degree.py @@ -0,0 +1,52 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from marshmallow import Schema, fields, post_load +from marshmallow.validate import OneOf, Length + +from flowmachine.features import SubscriberDegree +from .base_exposed_query import BaseExposedQuery +from .custom_fields import SubscriberSubset + +__all__ = ["SubscriberDegreeSchema", "SubscriberDegreeExposed"] + + +class SubscriberDegreeSchema(Schema): + query_kind = fields.String(validate=OneOf(["subscriber_degree"])) + start = fields.Date(required=True) + stop = fields.Date(required=True) + direction = fields.String( + required=False, validate=OneOf(["in", "out", "both"]), default="both" + ) # TODO: use a globally defined enum for this + subscriber_subset = SubscriberSubset() + + @post_load + def make_query_object(self, params, **kwargs): + return SubscriberDegreeExposed(**params) + + +class SubscriberDegreeExposed(BaseExposedQuery): + def __init__(self, *, start, stop, direction, subscriber_subset=None): + # Note: all input parameters need to be defined as attributes on `self` + # so that marshmallow can serialise the object correctly. + self.start = start + self.stop = stop + self.direction = direction + self.subscriber_subset = subscriber_subset + + @property + def _flowmachine_query_obj(self): + """ + Return the underlying flowmachine subscriber_degree object. + + Returns + ------- + Query + """ + return SubscriberDegree( + start=self.start, + stop=self.stop, + direction=self.direction, + subscriber_subset=self.subscriber_subset, + ) diff --git a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_json_spec.approved.txt b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_json_spec.approved.txt index b86444dda2..525b72ac3f 100644 --- a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_json_spec.approved.txt +++ b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_json_spec.approved.txt @@ -335,6 +335,7 @@ "discriminator": { "mapping": { "radius_of_gyration": "#/components/schemas/RadiusOfGyration", + "subscriber_degree": "#/components/schemas/SubscriberDegree", "unique_location_counts": "#/components/schemas/UniqueLocationCounts" }, "propertyName": "query_kind" @@ -343,6 +344,9 @@ { "$ref": "#/components/schemas/RadiusOfGyration" }, + { + "$ref": "#/components/schemas/SubscriberDegree" + }, { "$ref": "#/components/schemas/UniqueLocationCounts" } @@ -842,6 +846,45 @@ ], "type": "object" }, + "SubscriberDegree": { + "properties": { + "direction": { + "enum": [ + "both", + "in", + "out" + ], + "type": "string" + }, + "query_kind": { + "enum": [ + "subscriber_degree" + ], + "type": "string" + }, + "start": { + "format": "date", + "type": "string" + }, + "stop": { + "format": "date", + "type": "string" + }, + "subscriber_subset": { + "enum": [ + null + ], + "nullable": true, + "type": "string" + } + }, + "required": [ + "query_kind", + "start", + "stop" + ], + "type": "object" + }, "TotalNetworkObjects": { "properties": { "aggregation_unit": { diff --git a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_redoc_spec.approved.txt b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_redoc_spec.approved.txt index 6b1c92f6aa..dd41e417d3 100644 --- a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_redoc_spec.approved.txt +++ b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_redoc_spec.approved.txt @@ -291,6 +291,9 @@ { "$ref": "#/components/schemas/RadiusOfGyration" }, + { + "$ref": "#/components/schemas/SubscriberDegree" + }, { "$ref": "#/components/schemas/UniqueLocationCounts" } @@ -790,6 +793,45 @@ ], "type": "object" }, + "SubscriberDegree": { + "properties": { + "direction": { + "enum": [ + "both", + "in", + "out" + ], + "type": "string" + }, + "query_kind": { + "enum": [ + "subscriber_degree" + ], + "type": "string" + }, + "start": { + "format": "date", + "type": "string" + }, + "stop": { + "format": "date", + "type": "string" + }, + "subscriber_subset": { + "enum": [ + null + ], + "nullable": true, + "type": "string" + } + }, + "required": [ + "query_kind", + "start", + "stop" + ], + "type": "object" + }, "TotalNetworkObjects": { "properties": { "aggregation_unit": { diff --git a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_yaml_spec.approved.txt b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_yaml_spec.approved.txt index b86444dda2..525b72ac3f 100644 --- a/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_yaml_spec.approved.txt +++ b/integration_tests/tests/flowapi_tests/test_api_spec.test_generated_openapi_yaml_spec.approved.txt @@ -335,6 +335,7 @@ "discriminator": { "mapping": { "radius_of_gyration": "#/components/schemas/RadiusOfGyration", + "subscriber_degree": "#/components/schemas/SubscriberDegree", "unique_location_counts": "#/components/schemas/UniqueLocationCounts" }, "propertyName": "query_kind" @@ -343,6 +344,9 @@ { "$ref": "#/components/schemas/RadiusOfGyration" }, + { + "$ref": "#/components/schemas/SubscriberDegree" + }, { "$ref": "#/components/schemas/UniqueLocationCounts" } @@ -842,6 +846,45 @@ ], "type": "object" }, + "SubscriberDegree": { + "properties": { + "direction": { + "enum": [ + "both", + "in", + "out" + ], + "type": "string" + }, + "query_kind": { + "enum": [ + "subscriber_degree" + ], + "type": "string" + }, + "start": { + "format": "date", + "type": "string" + }, + "stop": { + "format": "date", + "type": "string" + }, + "subscriber_subset": { + "enum": [ + null + ], + "nullable": true, + "type": "string" + } + }, + "required": [ + "query_kind", + "start", + "stop" + ], + "type": "object" + }, "TotalNetworkObjects": { "properties": { "aggregation_unit": { diff --git a/integration_tests/tests/flowmachine_server_tests/test_server.test_api_spec_of_flowmachine_query_schemas.approved.txt b/integration_tests/tests/flowmachine_server_tests/test_server.test_api_spec_of_flowmachine_query_schemas.approved.txt index 80489b8534..6be20c51fb 100644 --- a/integration_tests/tests/flowmachine_server_tests/test_server.test_api_spec_of_flowmachine_query_schemas.approved.txt +++ b/integration_tests/tests/flowmachine_server_tests/test_server.test_api_spec_of_flowmachine_query_schemas.approved.txt @@ -327,6 +327,7 @@ "discriminator": { "mapping": { "radius_of_gyration": "#/components/schemas/RadiusOfGyration", + "subscriber_degree": "#/components/schemas/SubscriberDegree", "unique_location_counts": "#/components/schemas/UniqueLocationCounts" }, "propertyName": "query_kind" @@ -335,6 +336,9 @@ { "$ref": "#/components/schemas/RadiusOfGyration" }, + { + "$ref": "#/components/schemas/SubscriberDegree" + }, { "$ref": "#/components/schemas/UniqueLocationCounts" } @@ -825,6 +829,44 @@ ], "type": "object" }, + "SubscriberDegree": { + "properties": { + "direction": { + "enum": [ + "both", + "in", + "out" + ], + "type": "string" + }, + "query_kind": { + "enum": [ + "subscriber_degree" + ], + "type": "string" + }, + "start": { + "format": "date", + "type": "string" + }, + "stop": { + "format": "date", + "type": "string" + }, + "subscriber_subset": { + "enum": [ + null + ], + "nullable": true, + "type": "string" + } + }, + "required": [ + "start", + "stop" + ], + "type": "object" + }, "TotalNetworkObjects": { "properties": { "aggregation_unit": { diff --git a/integration_tests/tests/query_tests/test_queries.py b/integration_tests/tests/query_tests/test_queries.py index 6e79ae9413..542b1e8e4c 100644 --- a/integration_tests/tests/query_tests/test_queries.py +++ b/integration_tests/tests/query_tests/test_queries.py @@ -119,6 +119,17 @@ ), }, ), + ( + "joined_spatial_aggregate", + { + "locations": flowclient.daily_location( + date="2016-01-01", aggregation_unit="admin3", method="last" + ), + "metric": flowclient.subscriber_degree( + start="2016-01-01", stop="2016-01-02", direction="both" + ), + }, + ), ( "joined_spatial_aggregate", {