"""Lambda publishing_client_report. This lambda is responsible for generating the publishing client reports, which are then made available to clients in OA as Excel files. The reports contain royalties by composer and royalties by song, for a specific label and period. This lambda can be invoked in two modes: 1. Producer mode: The lambda will query the database for all available labels and periods, and produce a report for each label and period. A label and period are available if the label has royalties for that period. All available labels and periods will then be sent to an SNS topic, which is subscribed to by the consumer lambda. To use this mode, the lambda should be invoked with NO PARAMETERS. 2. Consumer mode: The lambda behaves as a consumer of the SNS topic, and will receive messages from the producer lambda. Each message will contain a label ID and a period ID, and the lambda will generate a report for that label and period. This mode will first fetch from Snowflake the royalties data for the given label and period, and then generate the report in Excel format, finally uploading it to S3. To use this mode, the lambda should be invoked with the following parameters: - label_id: The label ID for which to produce the report. - period_id: The period ID for which to produce the report. See the `QueryEvent` model for more details. For increased efficiency, the lambda uses a thread pool executor to run the SQL queries concurrently. The executor is shared between the producer and consumer logic, so that it can be reused during the container lifecycle. Some other costly operations are also run once and cached for the entire lifecycle of the container. """ from pydantic import BaseModel, Field, model_validator from common.src import logger from common.src.aws.utils import json_or_sns_payload from common.src.aws import responses from .logic.producer import handler as producer_handler from .logic.consumer import handler as consumer_handler logger = logger.new_logger(__name__) class QueryEvent(BaseModel): """Lambda payload model.""" label_id: int | None = Field( None, description="Label ID for which to produce publishing report" ) period_id: int | None = Field( None, description="Period ID for which to produce publishing report" ) @model_validator(mode="after") def check_label_and_period(self) -> "QueryEvent": """Enforce that label_id and period_id are both provided or both omitted, and both > 0 if provided. """ if (self.label_id is None) != (self.period_id is None): raise ValueError( "label_id and period_id must both be provided or both omitted" ) if self.label_id is not None and self.label_id <= 0: raise ValueError("label_id must be greater than 0") if self.period_id is not None and self.period_id <= 0: raise ValueError("period_id must be greater than 0") return self def handler(event, *_): """Lambda entry point. It allows invoking the function with a JSON payload or an SNS message. If via SNS, the message string should have JSON syntax and match the QueryEvent schema. """ # Normalize the payload to JSON, so that it can be invoked with a JSON payload # or an SNS message. payload = json_or_sns_payload(event) payload = QueryEvent(**payload) logger.debug(f"Received payload: {payload.model_dump_json()}") is_producer_mode: bool = payload.label_id is None and payload.period_id is None logger.info(f"Executing in {'PRODUCER' if is_producer_mode else 'CONSUMER'} mode.") if is_producer_mode: producer_handler.run() else: consumer_handler.run(payload.label_id, payload.period_id) return responses.http_200()