From 2455f009c830f25549eaee33a30bc336969ac483 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sat, 13 Jun 2020 18:48:06 +0100 Subject: [PATCH 1/8] chore: move blockquotes as hidden comments --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 49d74d706e1..5f770c58824 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,11 @@ ## Description of changes: -> One or two sentences as a summary of what's being changed + + +**Checklist** + + * [ ] [Meet tenets criteria](https://awslabs.github.io/aws-lambda-powertools-python/#tenets) * [ ] Update tests @@ -11,7 +15,7 @@ ## Breaking change checklist -> Ignore if it's not a breaking change + **RFC issue #**: From 57d290525a75891c4ec10dda5eb62e1ca1972d96 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Sun, 14 Jun 2020 13:21:31 +0100 Subject: [PATCH 2/8] docs: customize contributing guide (#77) * docs: clean up contributing guide Signed-off-by: heitorlessa * docs: add local documentation section Signed-off-by: heitorlessa * feat: experiment with Gitpod pre-build env Signed-off-by: heitorlessa * fix: unnecessary exc_info in example exception * docs: add conventions we use across the project Signed-off-by: heitorlessa * chore: remove gitpod due to poetry issue * chore: develop branch in bold Signed-off-by: heitorlessa * chore: add link to RFC issue, fix typo Signed-off-by: heitorlessa * chore: address Nicolas feedback on PR link Signed-off-by: heitorlessa --- CONTRIBUTING.md | 52 +++++++++++++++++++++++++------------- example/hello_world/app.py | 2 +- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914e0741d75..45c5e93da78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,50 +6,66 @@ documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. - ## Reporting Bugs/Feature Requests -We welcome you to use the GitHub issue tracker to report bugs or suggest features. +We welcome you to use the GitHub issue tracker to report bugs, suggest features, or documentation improvements. When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already -reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: - -* A reproducible test case or series of steps -* The version of our code being used -* Any modifications you've made relevant to the bug -* Anything unusual about your environment or deployment - +reported the issue. Please try to include as much information as you can. ## Contributing via Pull Requests + Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: -1. You are working against the latest source on the *master* branch. -2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. -3. You open an issue to discuss any significant work - we would hate for your time to be wasted. +1. You are working against the latest source on the **develop** branch. +2. You check existing open, and recently merged pull requests to make sure someone else hasn't addressed the problem already. +3. You open a [RFC issue](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=RFC%2C+triage&template=rfc.md&title=RFC%3A+) to discuss any significant work - we would hate for your time to be wasted. + +### Dev setup -To send us a pull request, please: +To send us a pull request, please follow these steps: 1. Fork the repository. -2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. -3. Ensure local tests pass. +2. Install dependencies in a virtual env with poetry, and pre-commit hooks: `make dev` +3. Create a new branch to focus on the specific change you are contributing e.g. `improv/logger-debug-sampling` +4. Run all tests, and code baseline checks: `make pr` + - Git hooks will run linting and formatting while `make pr` run deep checks that also run in the CI process 4. Commit to your fork using clear commit messages. -5. Send us a pull request, answering any default questions in the pull request interface. +5. Send us a pull request with a [conventional semantic title](https://github.com/awslabs/aws-lambda-powertools-python/pull/67), and answering any default questions in the pull request interface. 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). +#### Local documentation + +You might find useful to run both the documentation website and the API reference locally while contributing: + +* **API reference**: `make docs-api-local` +* **Docs website**: `make dev-docs` to install deps, and `make docs-local` to run it thereafter + +### Conventions + +Category | Convention +------------------------------------------------- | --------------------------------------------------------------------------------- +**Docstring** | We use a slight variation of numpy convention with markdown to help generate more readable API references. +**Style guide** | We use black as well as flake8 extensions to enforce beyond good practices [PEP8](https://pep8.org/). We strive to make use of type annotation as much as possible, but don't overdo in creating custom types. +**Core utilities** | Core utilities use a Class, always accept `service` as a constructor parameter, can work in isolation, and are also available in other languages implementation. +**Utilities** | Utilities are not as strict as core and focus on solving a developer experience problem while following the project [Tenets](https://awslabs.github.io/aws-lambda-powertools-python/#tenets). +**Exceptions** | Specific exceptions live within utilities themselves and use `Error` suffix e.g. `MetricUnitError`. +**Git commits** | We follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). These are not enforced as we squash and merge PRs, but PR titles are enforced during CI. +**Documentation** | API reference docs are generated from docstrings which should have Examples section to allow developers to have what they need within their own IDE. Documentation website covers the wider usage, tips, and strive to be concise. ## Finding contributions to work on -Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/help wanted/invalid/question/documentation), looking at any 'help wanted' issues is a great place to start. ## Code of Conduct + This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. - ## Security issue notifications If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. diff --git a/example/hello_world/app.py b/example/hello_world/app.py index 53ce0ace715..3d47646c389 100644 --- a/example/hello_world/app.py +++ b/example/hello_world/app.py @@ -117,7 +117,7 @@ def lambda_handler(event, context): metrics.add_metric(name="SuccessfulLocations", unit="Count", value=1) except requests.RequestException as e: # Send some context about this error to Lambda Logs - logger.exception(e, exc_info=True) + logger.exception(e) raise with single_metric(name="UniqueMetricDimension", unit="Seconds", value=1) as metric: From 69d5fda486644cc0106679ea9a75ca014236e308 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Mon, 15 Jun 2020 10:17:31 +0100 Subject: [PATCH 3/8] chore: remove deprecated code before GA (#78) * chore: remove Logger deprecated code * chore: remove Metrics deprecated code * chore: remove models from deprecated code * chore: move logger formatter to its own file --- aws_lambda_powertools/helper/__init__.py | 2 - aws_lambda_powertools/helper/models.py | 132 ---------- aws_lambda_powertools/logging/__init__.py | 5 +- aws_lambda_powertools/logging/formatter.py | 99 +++++++ .../logging/lambda_context.py | 55 ++++ aws_lambda_powertools/logging/logger.py | 247 +----------------- aws_lambda_powertools/metrics/__init__.py | 5 +- aws_lambda_powertools/metrics/base.py | 60 +++-- aws_lambda_powertools/metrics/exceptions.py | 6 - aws_lambda_powertools/metrics/metric.py | 3 +- tests/functional/test_logger.py | 89 ------- tests/functional/test_metrics.py | 155 +++-------- 12 files changed, 234 insertions(+), 624 deletions(-) delete mode 100644 aws_lambda_powertools/helper/__init__.py delete mode 100644 aws_lambda_powertools/helper/models.py create mode 100644 aws_lambda_powertools/logging/formatter.py create mode 100644 aws_lambda_powertools/logging/lambda_context.py diff --git a/aws_lambda_powertools/helper/__init__.py b/aws_lambda_powertools/helper/__init__.py deleted file mode 100644 index eb6356d8ec9..00000000000 --- a/aws_lambda_powertools/helper/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Collection of reusable code shared across powertools utilities -""" diff --git a/aws_lambda_powertools/helper/models.py b/aws_lambda_powertools/helper/models.py deleted file mode 100644 index bc6ea23eaa6..00000000000 --- a/aws_lambda_powertools/helper/models.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Collection of classes as models and builder functions -that provide classes as data representation for -key data used in more than one place. -""" - -from enum import Enum -from typing import Union - - -class LambdaContextModel: - """A handful of Lambda Runtime Context fields - - Full Lambda Context object: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html - - NOTE - ---- - - Originally, memory_size is `int` but we cast to `str` in this model - due to aws_lambda_logging library use of `%` during formatting - Ref: https://gitlab.com/hadrien/aws_lambda_logging/blob/master/aws_lambda_logging.py#L47 - - Parameters - ---------- - function_name: str - Lambda function name, by default "UNDEFINED" - e.g. "test" - function_memory_size: str - Lambda function memory in MB, by default "UNDEFINED" - e.g. "128" - casting from int to str due to aws_lambda_logging using `%` when enumerating fields - function_arn: str - Lambda function ARN, by default "UNDEFINED" - e.g. "arn:aws:lambda:eu-west-1:809313241:function:test" - function_request_id: str - Lambda function unique request id, by default "UNDEFINED" - e.g. "52fdfc07-2182-154f-163f-5f0f9a621d72" - """ - - def __init__( - self, - function_name: str = "UNDEFINED", - function_memory_size: str = "UNDEFINED", - function_arn: str = "UNDEFINED", - function_request_id: str = "UNDEFINED", - ): - self.function_name = function_name - self.function_memory_size = function_memory_size - self.function_arn = function_arn - self.function_request_id = function_request_id - - -def build_lambda_context_model(context: object) -> LambdaContextModel: - """Captures Lambda function runtime info to be used across all log statements - - Parameters - ---------- - context : object - Lambda context object - - Returns - ------- - LambdaContextModel - Lambda context only with select fields - """ - - context = { - "function_name": context.function_name, - "function_memory_size": context.memory_limit_in_mb, - "function_arn": context.invoked_function_arn, - "function_request_id": context.aws_request_id, - } - - return LambdaContextModel(**context) - - -class MetricUnit(Enum): - Seconds = "Seconds" - Microseconds = "Microseconds" - Milliseconds = "Milliseconds" - Bytes = "Bytes" - Kilobytes = "Kilobytes" - Megabytes = "Megabytes" - Gigabytes = "Gigabytes" - Terabytes = "Terabytes" - Bits = "Bits" - Kilobits = "Kilobits" - Megabits = "Megabits" - Gigabits = "Gigabits" - Terabits = "Terabits" - Percent = "Percent" - Count = "Count" - BytesPerSecond = "Bytes/Second" - KilobytesPerSecond = "Kilobytes/Second" - MegabytesPerSecond = "Megabytes/Second" - GigabytesPerSecond = "Gigabytes/Second" - TerabytesPerSecond = "Terabytes/Second" - BitsPerSecond = "Bits/Second" - KilobitsPerSecond = "Kilobits/Second" - MegabitsPerSecond = "Megabits/Second" - GigabitsPerSecond = "Gigabits/Second" - TerabitsPerSecond = "Terabits/Second" - CountPerSecond = "Count/Second" - - -def build_metric_unit_from_str(unit: Union[str, MetricUnit]) -> MetricUnit: - """Builds correct metric unit value from string or return Count as default - - Parameters - ---------- - unit : str, MetricUnit - metric unit - - Returns - ------- - MetricUnit - Metric Unit enum from string value or MetricUnit.Count as a default - """ - if isinstance(unit, MetricUnit): - return unit - - if isinstance(unit, str): - unit = unit.lower().capitalize() - - metric_unit = None - - try: - metric_unit = MetricUnit[unit] - except (TypeError, KeyError): - metric_units = [units for units, _ in MetricUnit.__members__.items()] - raise ValueError(f"Invalid Metric Unit - Received {unit}. Value Metric Units are {metric_units}") - - return metric_unit diff --git a/aws_lambda_powertools/logging/__init__.py b/aws_lambda_powertools/logging/__init__.py index 4c1bb2ec5c6..0456b202ffa 100644 --- a/aws_lambda_powertools/logging/__init__.py +++ b/aws_lambda_powertools/logging/__init__.py @@ -1,6 +1,5 @@ """Logging utility """ -from ..helper.models import MetricUnit -from .logger import Logger, log_metric, logger_inject_lambda_context, logger_setup +from .logger import Logger -__all__ = ["logger_setup", "logger_inject_lambda_context", "log_metric", "MetricUnit", "Logger"] +__all__ = ["Logger"] diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py new file mode 100644 index 00000000000..8aa07069f97 --- /dev/null +++ b/aws_lambda_powertools/logging/formatter.py @@ -0,0 +1,99 @@ +import json +import logging +from typing import Any + + +def json_formatter(unserialized_value: Any): + """JSON custom serializer to cast unserialisable values to strings. + + Example + ------- + + **Serialize unserialisable value to string** + + class X: pass + value = {"x": X()} + + json.dumps(value, default=json_formatter) + + Parameters + ---------- + unserialized_value: Any + Python object unserializable by JSON + """ + return str(unserialized_value) + + +class JsonFormatter(logging.Formatter): + """AWS Lambda Logging formatter. + + Formats the log message as a JSON encoded string. If the message is a + dict it will be used directly. If the message can be parsed as JSON, then + the parse d value is used in the output record. + + Originally taken from https://gitlab.com/hadrien/aws_lambda_logging/ + + """ + + def __init__(self, **kwargs): + """Return a JsonFormatter instance. + + The `json_default` kwarg is used to specify a formatter for otherwise + unserialisable values. It must not throw. Defaults to a function that + coerces the value to a string. + + Other kwargs are used to specify log field format strings. + """ + datefmt = kwargs.pop("datefmt", None) + + super(JsonFormatter, self).__init__(datefmt=datefmt) + self.reserved_keys = ["timestamp", "level", "location"] + self.format_dict = { + "timestamp": "%(asctime)s", + "level": "%(levelname)s", + "location": "%(funcName)s:%(lineno)d", + } + self.format_dict.update(kwargs) + self.default_json_formatter = kwargs.pop("json_default", json_formatter) + + def format(self, record): # noqa: A003 + record_dict = record.__dict__.copy() + record_dict["asctime"] = self.formatTime(record, self.datefmt) + + log_dict = {} + for key, value in self.format_dict.items(): + if value and key in self.reserved_keys: + # converts default logging expr to its record value + # e.g. '%(asctime)s' to '2020-04-24 09:35:40,698' + log_dict[key] = value % record_dict + else: + log_dict[key] = value + + if isinstance(record_dict["msg"], dict): + log_dict["message"] = record_dict["msg"] + else: + log_dict["message"] = record.getMessage() + + # Attempt to decode the message as JSON, if so, merge it with the + # overall message for clarity. + try: + log_dict["message"] = json.loads(log_dict["message"]) + except (json.decoder.JSONDecodeError, TypeError, ValueError): + pass + + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + # from logging.Formatter:format + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + log_dict["exception"] = record.exc_text + + json_record = json.dumps(log_dict, default=self.default_json_formatter) + + if hasattr(json_record, "decode"): # pragma: no cover + json_record = json_record.decode("utf-8") + + return json_record diff --git a/aws_lambda_powertools/logging/lambda_context.py b/aws_lambda_powertools/logging/lambda_context.py new file mode 100644 index 00000000000..75da8711f03 --- /dev/null +++ b/aws_lambda_powertools/logging/lambda_context.py @@ -0,0 +1,55 @@ +class LambdaContextModel: + """A handful of Lambda Runtime Context fields + + Full Lambda Context object: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + + Parameters + ---------- + function_name: str + Lambda function name, by default "UNDEFINED" + e.g. "test" + function_memory_size: int + Lambda function memory in MB, by default 128 + function_arn: str + Lambda function ARN, by default "UNDEFINED" + e.g. "arn:aws:lambda:eu-west-1:809313241:function:test" + function_request_id: str + Lambda function unique request id, by default "UNDEFINED" + e.g. "52fdfc07-2182-154f-163f-5f0f9a621d72" + """ + + def __init__( + self, + function_name: str = "UNDEFINED", + function_memory_size: int = 128, + function_arn: str = "UNDEFINED", + function_request_id: str = "UNDEFINED", + ): + self.function_name = function_name + self.function_memory_size = function_memory_size + self.function_arn = function_arn + self.function_request_id = function_request_id + + +def build_lambda_context_model(context: object) -> LambdaContextModel: + """Captures Lambda function runtime info to be used across all log statements + + Parameters + ---------- + context : object + Lambda context object + + Returns + ------- + LambdaContextModel + Lambda context only with select fields + """ + + context = { + "function_name": context.function_name, + "function_memory_size": context.memory_limit_in_mb, + "function_arn": context.invoked_function_arn, + "function_request_id": context.aws_request_id, + } + + return LambdaContextModel(**context) diff --git a/aws_lambda_powertools/logging/logger.py b/aws_lambda_powertools/logging/logger.py index 63fa6ed8b28..9a943536b4e 100644 --- a/aws_lambda_powertools/logging/logger.py +++ b/aws_lambda_powertools/logging/logger.py @@ -1,157 +1,21 @@ import copy import functools -import itertools -import json import logging import os import random import sys -import warnings from distutils.util import strtobool from typing import Any, Callable, Dict, Union -from ..helper.models import MetricUnit, build_lambda_context_model, build_metric_unit_from_str from .exceptions import InvalidLoggerSamplingRateError +from .formatter import JsonFormatter +from .lambda_context import build_lambda_context_model logger = logging.getLogger(__name__) is_cold_start = True -def json_formatter(unserialized_value: Any): - """JSON custom serializer to cast unserialisable values to strings. - - Example - ------- - - **Serialize unserialisable value to string** - - class X: pass - value = {"x": X()} - - json.dumps(value, default=json_formatter) - - Parameters - ---------- - unserialized_value: Any - Python object unserializable by JSON - """ - return str(unserialized_value) - - -class JsonFormatter(logging.Formatter): - """AWS Lambda Logging formatter. - - Formats the log message as a JSON encoded string. If the message is a - dict it will be used directly. If the message can be parsed as JSON, then - the parse d value is used in the output record. - - Originally taken from https://gitlab.com/hadrien/aws_lambda_logging/ - - """ - - def __init__(self, **kwargs): - """Return a JsonFormatter instance. - - The `json_default` kwarg is used to specify a formatter for otherwise - unserialisable values. It must not throw. Defaults to a function that - coerces the value to a string. - - Other kwargs are used to specify log field format strings. - """ - datefmt = kwargs.pop("datefmt", None) - - super(JsonFormatter, self).__init__(datefmt=datefmt) - self.reserved_keys = ["timestamp", "level", "location"] - self.format_dict = { - "timestamp": "%(asctime)s", - "level": "%(levelname)s", - "location": "%(funcName)s:%(lineno)d", - } - self.format_dict.update(kwargs) - self.default_json_formatter = kwargs.pop("json_default", json_formatter) - - def format(self, record): # noqa: A003 - record_dict = record.__dict__.copy() - record_dict["asctime"] = self.formatTime(record, self.datefmt) - - log_dict = {} - for key, value in self.format_dict.items(): - if value and key in self.reserved_keys: - # converts default logging expr to its record value - # e.g. '%(asctime)s' to '2020-04-24 09:35:40,698' - log_dict[key] = value % record_dict - else: - log_dict[key] = value - - if isinstance(record_dict["msg"], dict): - log_dict["message"] = record_dict["msg"] - else: - log_dict["message"] = record.getMessage() - - # Attempt to decode the message as JSON, if so, merge it with the - # overall message for clarity. - try: - log_dict["message"] = json.loads(log_dict["message"]) - except (json.decoder.JSONDecodeError, TypeError, ValueError): - pass - - if record.exc_info: - # Cache the traceback text to avoid converting it multiple times - # (it's constant anyway) - # from logging.Formatter:format - if not record.exc_text: - record.exc_text = self.formatException(record.exc_info) - - if record.exc_text: - log_dict["exception"] = record.exc_text - - json_record = json.dumps(log_dict, default=self.default_json_formatter) - - if hasattr(json_record, "decode"): # pragma: no cover - json_record = json_record.decode("utf-8") - - return json_record - - -def logger_setup( - service: str = None, level: str = None, sampling_rate: float = 0.0, legacy: bool = False, **kwargs -) -> DeprecationWarning: - """DEPRECATED - - This will be removed when GA - Use `aws_lambda_powertools.logging.logger.Logger` instead - - Example - ------- - **Logger class - Same UX** - - from aws_lambda_powertools import Logger - logger = Logger(service="payment") # same env var still applies - - """ - raise DeprecationWarning("Use Logger instead - This method will be removed when GA") - - -def logger_inject_lambda_context( - lambda_handler: Callable[[Dict, Any], Any] = None, log_event: bool = False -) -> DeprecationWarning: - """DEPRECATED - - This will be removed when GA - Use `aws_lambda_powertools.logging.logger.Logger` instead - - Example - ------- - **Logger class - Same UX** - - from aws_lambda_powertools import Logger - logger = Logger(service="payment") # same env var still applies - @logger.inject_lambda_context - def handler(evt, ctx): - pass - """ - raise DeprecationWarning("Use Logger instead - This method will be removed when GA") - - def _is_cold_start() -> bool: """Verifies whether is cold start @@ -170,113 +34,6 @@ def _is_cold_start() -> bool: return cold_start -def log_metric( - name: str, namespace: str, unit: MetricUnit, value: float = 0, service: str = "service_undefined", **dimensions, -): - """Logs a custom metric in a statsD-esque format to stdout. - - **This will be removed when GA - Use `aws_lambda_powertools.metrics.metrics.Metrics` instead** - - Creating Custom Metrics synchronously impact on performance/execution time. - Instead, log_metric prints a metric to CloudWatch Logs. - That allows us to pick them up asynchronously via another Lambda function and create them as a metric. - - NOTE: It takes up to 9 dimensions by default, and Metric units are conveniently available via MetricUnit Enum. - If service is not passed as arg or via env var, "service_undefined" will be used as dimension instead. - - **Output in CloudWatch Logs**: `MONITORING|||||` - - Serverless Application Repository App that creates custom metric from this log output: - https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:374852340823:applications~async-custom-metrics - - Environment variables - --------------------- - POWERTOOLS_SERVICE_NAME: str - service name - - Parameters - ---------- - name : str - metric name, by default None - namespace : str - metric namespace (e.g. application name), by default None - unit : MetricUnit, by default MetricUnit.Count - metric unit enum value (e.g. MetricUnit.Seconds), by default None\n - API Info: https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html - value : float, optional - metric value, by default 0 - service : str, optional - service name used as dimension, by default "service_undefined" - dimensions: dict, optional - keyword arguments as additional dimensions (e.g. `customer=customerId`) - - Example - ------- - **Log metric to count number of successful payments; define service via env var** - - $ export POWERTOOLS_SERVICE_NAME="payment" - from aws_lambda_powertools.logging import MetricUnit, log_metric - log_metric( - name="SuccessfulPayments", - unit=MetricUnit.Count, - value=1, - namespace="DemoApp" - ) - - **Log metric to count number of successful payments per campaign & customer** - - from aws_lambda_powertools.logging import MetricUnit, log_metric - log_metric( - name="SuccessfulPayments", - service="payment", - unit=MetricUnit.Count, - value=1, - namespace="DemoApp", - campaign=campaign_id, - customer=customer_id - ) - """ - - warnings.warn(message="This method will be removed in GA; use Metrics instead", category=DeprecationWarning) - logger.debug(f"Building new custom metric. Name: {name}, Unit: {unit}, Value: {value}, Dimensions: {dimensions}") - service = os.getenv("POWERTOOLS_SERVICE_NAME") or service - dimensions = __build_dimensions(**dimensions) - unit = build_metric_unit_from_str(unit) - - metric = f"MONITORING|{value}|{unit.name}|{name}|{namespace}|service={service}" - if dimensions: - metric = f"MONITORING|{value}|{unit.name}|{name}|{namespace}|service={service},{dimensions}" - - print(metric) - - -def __build_dimensions(**dimensions) -> str: - """Builds correct format for custom metric dimensions from kwargs - - Parameters - ---------- - dimensions: dict, optional - additional dimensions - - Returns - ------- - str - Dimensions in the form of "key=value,key2=value2" - """ - MAX_DIMENSIONS = 10 - dimension = "" - - # CloudWatch accepts a max of 10 dimensions per metric - # We include service name as a dimension - # so we take up to 9 values as additional dimensions - # before we convert everything to a string of key=value - dimensions_partition = dict(itertools.islice(dimensions.items(), MAX_DIMENSIONS)) - dimensions_list = [dimension + "=" + value for dimension, value in dimensions_partition.items() if value] - dimension = ",".join(dimensions_list) - - return dimension - - class Logger(logging.Logger): """Creates and setups a logger to format statements in JSON. diff --git a/aws_lambda_powertools/metrics/__init__.py b/aws_lambda_powertools/metrics/__init__.py index 2f71957437d..7379dad8b88 100644 --- a/aws_lambda_powertools/metrics/__init__.py +++ b/aws_lambda_powertools/metrics/__init__.py @@ -1,7 +1,7 @@ """CloudWatch Embedded Metric Format utility """ -from ..helper.models import MetricUnit -from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError, UniqueNamespaceError +from .base import MetricUnit +from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError from .metric import single_metric from .metrics import Metrics @@ -12,5 +12,4 @@ "MetricUnitError", "SchemaValidationError", "MetricValueError", - "UniqueNamespaceError", ] diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index d6529cf71e2..1eece781bbf 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -4,13 +4,12 @@ import numbers import os import pathlib -import warnings +from enum import Enum from typing import Dict, List, Union import fastjsonschema -from ..helper.models import MetricUnit -from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError, UniqueNamespaceError +from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError logger = logging.getLogger(__name__) @@ -21,6 +20,35 @@ MAX_METRICS = 100 +class MetricUnit(Enum): + Seconds = "Seconds" + Microseconds = "Microseconds" + Milliseconds = "Milliseconds" + Bytes = "Bytes" + Kilobytes = "Kilobytes" + Megabytes = "Megabytes" + Gigabytes = "Gigabytes" + Terabytes = "Terabytes" + Bits = "Bits" + Kilobits = "Kilobits" + Megabits = "Megabits" + Gigabits = "Gigabits" + Terabits = "Terabits" + Percent = "Percent" + Count = "Count" + BytesPerSecond = "Bytes/Second" + KilobytesPerSecond = "Kilobytes/Second" + MegabytesPerSecond = "Megabytes/Second" + GigabytesPerSecond = "Gigabytes/Second" + TerabytesPerSecond = "Terabytes/Second" + BitsPerSecond = "Bits/Second" + KilobitsPerSecond = "Kilobits/Second" + MegabitsPerSecond = "Megabits/Second" + GigabitsPerSecond = "Gigabits/Second" + TerabitsPerSecond = "Terabits/Second" + CountPerSecond = "Count/Second" + + class MetricManager: """Base class for metric functionality (namespace, metric, dimension, serialization) @@ -45,8 +73,6 @@ class MetricManager: When metric metric isn't supported by CloudWatch MetricValueError When metric value isn't a number - UniqueNamespaceError - When an additional namespace is set SchemaValidationError When metric object fails EMF schema validation """ @@ -61,30 +87,6 @@ def __init__( self._metric_units = [unit.value for unit in MetricUnit] self._metric_unit_options = list(MetricUnit.__members__) - def add_namespace(self, name: str): - """Adds given metric namespace - - Example - ------- - **Add metric namespace** - - metric.add_namespace(name="ServerlessAirline") - - Parameters - ---------- - name : str - Metric namespace - """ - warnings.warn( - "add_namespace method is deprecated. Pass namespace to Metrics constructor instead", DeprecationWarning - ) - if self.namespace is not None: - raise UniqueNamespaceError( - f"Namespace '{self.namespace}' already set - Only one namespace is allowed across metrics" - ) - logger.debug(f"Adding metrics namespace: {name}") - self.namespace = name - def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]): """Adds given metric diff --git a/aws_lambda_powertools/metrics/exceptions.py b/aws_lambda_powertools/metrics/exceptions.py index 88a38c24229..0376c55a40e 100644 --- a/aws_lambda_powertools/metrics/exceptions.py +++ b/aws_lambda_powertools/metrics/exceptions.py @@ -14,9 +14,3 @@ class MetricValueError(Exception): """When metric value isn't a valid number""" pass - - -class UniqueNamespaceError(Exception): - """When an additional namespace is set""" - - pass diff --git a/aws_lambda_powertools/metrics/metric.py b/aws_lambda_powertools/metrics/metric.py index 53434b4a9d4..1293139afbe 100644 --- a/aws_lambda_powertools/metrics/metric.py +++ b/aws_lambda_powertools/metrics/metric.py @@ -3,8 +3,7 @@ from contextlib import contextmanager from typing import Dict -from ..helper.models import MetricUnit -from .base import MetricManager +from .base import MetricManager, MetricUnit logger = logging.getLogger(__name__) diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index 19c45a8587e..6b6a4bb6dde 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -6,7 +6,6 @@ import pytest from aws_lambda_powertools import Logger, Tracer -from aws_lambda_powertools.logging import MetricUnit, log_metric, logger_inject_lambda_context, logger_setup from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError from aws_lambda_powertools.logging.logger import JsonFormatter, set_package_logger @@ -236,68 +235,6 @@ def handler(event, context): assert fourth_log["cold_start"] is False -def test_log_metric(capsys): - # GIVEN a service, unit and value have been provided - # WHEN log_metric is called - # THEN custom metric line should be match given values - log_metric( - service="payment", name="test_metric", unit=MetricUnit.Seconds, value=60, namespace="DemoApp", - ) - expected = "MONITORING|60|Seconds|test_metric|DemoApp|service=payment\n" - captured = capsys.readouterr() - - assert captured.out == expected - - -def test_log_metric_env_var(monkeypatch, capsys): - # GIVEN a service, unit and value have been provided - # WHEN log_metric is called - # THEN custom metric line should be match given values - service_name = "payment" - monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", service_name) - - log_metric(name="test_metric", unit=MetricUnit.Seconds, value=60, namespace="DemoApp") - expected = "MONITORING|60|Seconds|test_metric|DemoApp|service=payment\n" - captured = capsys.readouterr() - - assert captured.out == expected - - -def test_log_metric_multiple_dimensions(capsys): - # GIVEN multiple optional dimensions are provided - # WHEN log_metric is called - # THEN dimensions should appear as dimenion=value - log_metric( - name="test_metric", unit=MetricUnit.Seconds, value=60, customer="abc", charge_id="123", namespace="DemoApp", - ) - expected = "MONITORING|60|Seconds|test_metric|DemoApp|service=service_undefined,customer=abc,charge_id=123\n" - captured = capsys.readouterr() - - assert captured.out == expected - - -@pytest.mark.parametrize( - "invalid_input,expected", - [ - ({"unit": "seconds"}, "MONITORING|0|Seconds|test_metric|DemoApp|service=service_undefined\n",), - ( - {"unit": "Seconds", "customer": None, "charge_id": "123", "payment_status": ""}, - "MONITORING|0|Seconds|test_metric|DemoApp|service=service_undefined,charge_id=123\n", - ), - ], - ids=["metric unit as string lower case", "empty dimension value"], -) -def test_log_metric_partially_correct_args(capsys, invalid_input, expected): - # GIVEN invalid arguments are provided such as empty dimension values and metric units in strings - # WHEN log_metric is called - # THEN default values should be used such as "Count" as a unit, invalid dimensions not included - # and no exception raised - log_metric(name="test_metric", namespace="DemoApp", **invalid_input) - captured = capsys.readouterr() - - assert captured.out == expected - - def test_package_logger(capsys): set_package_logger() @@ -315,32 +252,6 @@ def test_package_logger_format(stdout, capsys): assert "test" in output["formatter"] -@pytest.mark.parametrize( - "invalid_input,expected", - [({"unit": "Blah"}, ValueError), ({"unit": None}, ValueError), ({}, TypeError)], - ids=["invalid metric unit as str", "unit as None", "missing required unit"], -) -def test_log_metric_invalid_unit(capsys, invalid_input, expected): - # GIVEN invalid units are provided - # WHEN log_metric is called - # THEN ValueError exception should be raised - - with pytest.raises(expected): - log_metric(name="test_metric", namespace="DemoApp", **invalid_input) - - -def test_logger_setup_deprecated(): - # Should be removed when GA - with pytest.raises(DeprecationWarning): - logger_setup() - - -def test_logger_inject_lambda_context_deprecated(): - # Should be removed when GA - with pytest.raises(DeprecationWarning): - logger_inject_lambda_context() - - def test_logger_append_duplicated(stdout): logger = Logger(stream=stdout, request_id="value") logger.structure_logs(append=True, request_id="new_value") diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index 3e2ebe34fe1..244a56119cd 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -6,13 +6,7 @@ import pytest from aws_lambda_powertools import Metrics, single_metric -from aws_lambda_powertools.metrics import ( - MetricUnit, - MetricUnitError, - MetricValueError, - SchemaValidationError, - UniqueNamespaceError, -) +from aws_lambda_powertools.metrics import MetricUnit, MetricUnitError, MetricValueError, SchemaValidationError from aws_lambda_powertools.metrics.base import MetricManager @@ -59,11 +53,11 @@ def non_str_dimensions() -> List[Dict[str, Any]]: @pytest.fixture def namespace() -> Dict[str, str]: - return {"name": "test_namespace"} + return "test_namespace" @pytest.fixture -def a_hundred_metrics() -> List[Dict[str, str]]: +def a_hundred_metrics(namespace=namespace) -> List[Dict[str, str]]: metrics = [] for i in range(100): metrics.append({"name": f"metric_{i}", "unit": "Count", "value": 1}) @@ -71,13 +65,12 @@ def a_hundred_metrics() -> List[Dict[str, str]]: return metrics -def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: Dict) -> Dict: +def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: str) -> Dict: """ Helper function to build EMF object from a list of metrics, dimensions """ - my_metrics = MetricManager() + my_metrics = MetricManager(namespace=namespace) for dimension in dimensions: my_metrics.add_dimension(**dimension) - my_metrics.add_namespace(**namespace) for metric in metrics: my_metrics.add_metric(**metric) @@ -85,12 +78,11 @@ def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: Di return my_metrics.serialize_metric_set() -def serialize_single_metric(metric: Dict, dimension: Dict, namespace: Dict) -> Dict: +def serialize_single_metric(metric: Dict, dimension: Dict, namespace: str) -> Dict: """ Helper function to build EMF object from a given metric, dimension and namespace """ - my_metrics = MetricManager() + my_metrics = MetricManager(namespace=namespace) my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) - my_metrics.add_namespace(**namespace) return my_metrics.serialize_metric_set() @@ -103,11 +95,10 @@ def remove_timestamp(metrics: List): def test_single_metric_one_metric_only(capsys, metric, dimension, namespace): # GIVEN we attempt to add more than one metric # WHEN using single_metric context manager - with single_metric(**metric) as my_metric: + with single_metric(namespace=namespace, **metric) as my_metric: my_metric.add_metric(name="second_metric", unit="Count", value=1) my_metric.add_metric(name="third_metric", unit="Seconds", value=1) my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) output = json.loads(capsys.readouterr().out.strip()) expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) @@ -118,25 +109,9 @@ def test_single_metric_one_metric_only(capsys, metric, dimension, namespace): assert expected["_aws"] == output["_aws"] -def test_multiple_namespaces_exception(metric, dimension, namespace): - # GIVEN we attempt to add multiple namespaces - namespace_a = {"name": "OtherNamespace"} - namespace_b = {"name": "AnotherNamespace"} - - # WHEN an EMF object can only have one - # THEN we should raise UniqueNamespaceError exception - with pytest.raises(UniqueNamespaceError): - with single_metric(**metric) as my_metric: - my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) - my_metric.add_namespace(**namespace_a) - my_metric.add_namespace(**namespace_b) - - def test_log_metrics(capsys, metrics, dimensions, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() - my_metrics.add_namespace(**namespace) + my_metrics = Metrics(namespace=namespace) for metric in metrics: my_metrics.add_metric(**metric) for dimension in dimensions: @@ -164,7 +139,7 @@ def lambda_handler(evt, ctx): def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): # GIVEN we use POWERTOOLS_METRICS_NAMESPACE - monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace["name"]) + monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace) # WHEN creating a metric but don't explicitly # add a namespace @@ -185,7 +160,7 @@ def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): def test_service_env_var(monkeypatch, capsys, metric, namespace): # GIVEN we use POWERTOOLS_SERVICE_NAME monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", "test_service") - my_metrics = Metrics(namespace=namespace["name"]) + my_metrics = Metrics(namespace=namespace) # WHEN creating a metric but don't explicitly # add a dimension @@ -210,9 +185,8 @@ def lambda_handler(evt, context): def test_metrics_spillover(monkeypatch, capsys, metric, dimension, namespace, a_hundred_metrics): # GIVEN Metrics is initialized and we have over a hundred metrics to add - my_metrics = Metrics() + my_metrics = Metrics(namespace=namespace) my_metrics.add_dimension(**dimension) - my_metrics.add_namespace(**namespace) # WHEN we add more than 100 metrics for _metric in a_hundred_metrics: @@ -242,12 +216,11 @@ def test_metrics_spillover(monkeypatch, capsys, metric, dimension, namespace, a_ def test_log_metrics_should_invoke_function(metric, dimension, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() + my_metrics = Metrics(namespace=namespace) # WHEN log_metrics is used to serialize metrics @my_metrics.log_metrics def lambda_handler(evt, context): - my_metrics.add_namespace(**namespace) my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) return True @@ -266,7 +239,6 @@ def test_incorrect_metric_unit(metric, dimension, namespace): with pytest.raises(MetricUnitError): with single_metric(**metric) as my_metric: my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) def test_schema_no_namespace(metric, dimension): @@ -289,13 +261,11 @@ def test_schema_incorrect_value(metric, dimension, namespace): with pytest.raises(MetricValueError): with single_metric(**metric) as my_metric: my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) def test_schema_no_metrics(dimensions, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() - my_metrics.add_namespace(**namespace) + my_metrics = Metrics(namespace=namespace) # WHEN no metrics have been added # but a namespace and dimensions only @@ -317,18 +287,16 @@ def test_exceed_number_of_dimensions(metric, namespace): # THEN it should fail validation and raise SchemaValidationError with pytest.raises(SchemaValidationError): with single_metric(**metric) as my_metric: - my_metric.add_namespace(**namespace) for dimension in dimensions: my_metric.add_dimension(**dimension) def test_log_metrics_during_exception(capsys, metric, dimension, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() + my_metrics = Metrics(namespace=namespace) my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) - my_metrics.add_namespace(**namespace) # WHEN log_metrics is used to serialize metrics # but an error has been raised during handler execution @@ -347,18 +315,18 @@ def lambda_handler(evt, context): assert expected["_aws"] == output["_aws"] -def test_log_no_metrics_error_propagation(capsys, metric, dimension, namespace): +def test_log_metrics_raise_on_empty_metrics(capsys, metric, dimension, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() + my_metrics = Metrics(service="test_service", namespace=namespace) @my_metrics.log_metrics(raise_on_empty_metrics=True) def lambda_handler(evt, context): # WHEN log_metrics is used with raise_on_empty_metrics param and has no metrics - # and the function decorated also raised an exception - raise ValueError("Bubble up") + return True - # THEN the raised exception should be - with pytest.raises(SchemaValidationError): + # THEN the raised exception should be SchemaValidationError + # and specifically about the lack of Metrics + with pytest.raises(SchemaValidationError, match="_aws\.CloudWatchMetrics\[0\]\.Metrics"): # noqa: W605 lambda_handler({}, {}) @@ -370,9 +338,8 @@ def test_all_possible_metric_units(metric, dimension, namespace): metric["unit"] = unit.name # WHEN we iterate over all available metric unit keys from MetricUnit enum # THEN we raise no MetricUnitError nor SchemaValidationError - with single_metric(**metric) as my_metric: + with single_metric(namespace=namespace, **metric) as my_metric: my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) # WHEN we iterate over all available metric unit keys from MetricUnit enum all_metric_units = [unit.value for unit in MetricUnit] @@ -381,18 +348,17 @@ def test_all_possible_metric_units(metric, dimension, namespace): for unit in all_metric_units: metric["unit"] = unit # THEN we raise no MetricUnitError nor SchemaValidationError - with single_metric(**metric) as my_metric: + with single_metric(namespace=namespace, **metric) as my_metric: my_metric.add_dimension(**dimension) - my_metric.add_namespace(**namespace) def test_metrics_reuse_metric_set(metric, dimension, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() + my_metrics = Metrics(namespace=namespace) my_metrics.add_metric(**metric) # WHEN Metrics is initialized one more time - my_metrics_2 = Metrics() + my_metrics_2 = Metrics(namespace=namespace) # THEN Both class instances should have the same metric set assert my_metrics_2.metric_set == my_metrics.metric_set @@ -400,11 +366,10 @@ def test_metrics_reuse_metric_set(metric, dimension, namespace): def test_log_metrics_clear_metrics_after_invocation(metric, dimension, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() + my_metrics = Metrics(namespace=namespace) my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) - my_metrics.add_namespace(**namespace) # WHEN log_metrics is used to flush metrics from memory @my_metrics.log_metrics @@ -419,8 +384,7 @@ def lambda_handler(evt, context): def test_log_metrics_non_string_dimension_values(capsys, metrics, non_str_dimensions, namespace): # GIVEN Metrics is initialized and dimensions with non-string values are added - my_metrics = Metrics() - my_metrics.add_namespace(**namespace) + my_metrics = Metrics(namespace=namespace) for metric in metrics: my_metrics.add_metric(**metric) for dimension in non_str_dimensions: @@ -441,16 +405,9 @@ def lambda_handler(evt, ctx): assert isinstance(output[dimension["name"]], str) -def test_add_namespace_warns_for_deprecation(capsys, metrics, dimensions, namespace): - # GIVEN Metrics is initialized - my_metrics = Metrics() - with pytest.deprecated_call(): - my_metrics.add_namespace(**namespace) - - def test_log_metrics_with_explicit_namespace(capsys, metrics, dimensions, namespace): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace=namespace["name"]) + my_metrics = Metrics(service="test_service", namespace=namespace) for metric in metrics: my_metrics.add_metric(**metric) for dimension in dimensions: @@ -476,9 +433,9 @@ def lambda_handler(evt, ctx): assert expected == output -def test_log_metrics_with_implicit_dimensions(capsys, metrics): +def test_log_metrics_with_implicit_dimensions(capsys, metrics, namespace): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace="test_application") + my_metrics = Metrics(service="test_service", namespace=namespace) for metric in metrics: my_metrics.add_metric(**metric) @@ -492,9 +449,7 @@ def lambda_handler(evt, ctx): output = json.loads(capsys.readouterr().out.strip()) expected_dimensions = [{"name": "service", "value": "test_service"}] - expected = serialize_metrics( - metrics=metrics, dimensions=expected_dimensions, namespace={"name": "test_application"} - ) + expected = serialize_metrics(metrics=metrics, dimensions=expected_dimensions, namespace=namespace) remove_timestamp(metrics=[output, expected]) # Timestamp will always be different @@ -530,37 +485,15 @@ def lambda_handler(evt, ctx): assert second_output["service"] == "another_test_service" -def test_log_metrics_with_namespace_overridden(capsys, metrics, dimensions): - # GIVEN Metrics is initialized with namespace specified - my_metrics = Metrics(namespace="test_service") - for metric in metrics: - my_metrics.add_metric(**metric) - for dimension in dimensions: - my_metrics.add_dimension(**dimension) - - # WHEN we try to call add_namespace - # THEN we should raise UniqueNamespaceError exception - @my_metrics.log_metrics - def lambda_handler(evt, ctx): - my_metrics.add_namespace(name="new_namespace") - return True - - with pytest.raises(UniqueNamespaceError): - lambda_handler({}, {}) - - with pytest.raises(UniqueNamespaceError): - my_metrics.add_namespace(name="another_new_namespace") - - -def test_single_metric_with_service(capsys, metric, dimension): +def test_single_metric_with_service(capsys, metric, dimension, namespace): # GIVEN we pass namespace parameter to single_metric # WHEN creating a metric - with single_metric(**metric, namespace="test_service") as my_metrics: + with single_metric(**metric, namespace=namespace) as my_metrics: my_metrics.add_dimension(**dimension) output = json.loads(capsys.readouterr().out.strip()) - expected = serialize_single_metric(metric=metric, dimension=dimension, namespace={"name": "test_service"}) + expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) remove_timestamp(metrics=[output, expected]) # Timestamp will always be different @@ -570,10 +503,10 @@ def test_single_metric_with_service(capsys, metric, dimension): def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension, namespace): # GIVEN we use POWERTOOLS_METRICS_NAMESPACE - monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace["name"]) + monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace) # WHEN creating a metric and explicitly set a namespace - with single_metric(**metric, namespace=namespace["name"]) as my_metrics: + with single_metric(namespace=namespace, **metric) as my_metrics: my_metrics.add_dimension(**dimension) monkeypatch.delenv("POWERTOOLS_METRICS_NAMESPACE") @@ -588,8 +521,7 @@ def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension, namesp def test_emit_cold_start_metric(capsys, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics() - my_metrics.add_namespace(**namespace) + my_metrics = Metrics(service="test_service", namespace=namespace) # WHEN log_metrics is used with capture_cold_start_metric @my_metrics.log_metrics(capture_cold_start_metric=True) @@ -608,8 +540,7 @@ def lambda_handler(evt, context): def test_emit_cold_start_metric_only_once(capsys, namespace, dimension, metric): # GIVEN Metrics is initialized - my_metrics = Metrics() - my_metrics.add_namespace(**namespace) + my_metrics = Metrics(namespace=namespace) # WHEN log_metrics is used with capture_cold_start_metric # and handler is called more than once @@ -635,7 +566,7 @@ def lambda_handler(evt, context): def test_log_metrics_decorator_no_metrics(dimensions, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace["name"], service="test_service") + my_metrics = Metrics(namespace=namespace, service="test_service") # WHEN using the log_metrics decorator and no metrics have been added @my_metrics.log_metrics @@ -649,9 +580,9 @@ def lambda_handler(evt, context): assert str(w[-1].message) == "No metrics to publish, skipping" -def test_log_metrics_with_implicit_dimensions_called_twice(capsys, metrics): +def test_log_metrics_with_implicit_dimensions_called_twice(capsys, metrics, namespace): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace="test_application") + my_metrics = Metrics(service="test_service", namespace=namespace) # WHEN we utilize log_metrics to serialize and don't explicitly add any dimensions, # and the lambda function is called more than once @@ -668,9 +599,7 @@ def lambda_handler(evt, ctx): second_output = json.loads(capsys.readouterr().out.strip()) expected_dimensions = [{"name": "service", "value": "test_service"}] - expected = serialize_metrics( - metrics=metrics, dimensions=expected_dimensions, namespace={"name": "test_application"} - ) + expected = serialize_metrics(metrics=metrics, dimensions=expected_dimensions, namespace=namespace) remove_timestamp(metrics=[output, expected, second_output]) # Timestamp will always be different From dd6a1e62ff6bfb8c2caecabb141d02d9fea72136 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Mon, 15 Jun 2020 18:34:53 +0100 Subject: [PATCH 4/8] chore: cleanup tests (#79) * chore: remove Logger deprecated code * chore: remove Metrics deprecated code * chore: remove models from deprecated code * chore: move logger formatter to its own file * chore: cleanup metrics tests * chore: cleanup tracer tests * chore: cleanup logger tests * chore: cleanup tracer tests * chore: set test coverage to 90% min Signed-off-by: heitorlessa --- aws_lambda_powertools/logging/formatter.py | 2 +- aws_lambda_powertools/tracing/tracer.py | 6 +- pyproject.toml | 1 + tests/functional/test_logger.py | 217 +++++++------- tests/functional/test_metrics.py | 324 ++++++++------------- tests/functional/test_tracing.py | 73 ++--- tests/unit/test_tracing.py | 109 +++++-- 7 files changed, 352 insertions(+), 380 deletions(-) diff --git a/aws_lambda_powertools/logging/formatter.py b/aws_lambda_powertools/logging/formatter.py index 8aa07069f97..7b38524b6d1 100644 --- a/aws_lambda_powertools/logging/formatter.py +++ b/aws_lambda_powertools/logging/formatter.py @@ -85,7 +85,7 @@ def format(self, record): # noqa: A003 # Cache the traceback text to avoid converting it multiple times # (it's constant anyway) # from logging.Formatter:format - if not record.exc_text: + if not record.exc_text: # pragma: no cover record.exc_text = self.formatException(record.exc_info) if record.exc_text: diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index d31cbd61ebd..b97816f25b5 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -269,8 +269,10 @@ def decorate(event, context): function_name=lambda_handler_name, data=response, subsegment=subsegment ) except Exception as err: - logger.exception("Exception received from lambda handler") - self._add_full_exception_as_metadata(function_name=self.service, error=err, subsegment=subsegment) + logger.exception(f"Exception received from {lambda_handler_name}") + self._add_full_exception_as_metadata( + function_name=lambda_handler_name, error=err, subsegment=subsegment + ) raise return response diff --git a/pyproject.toml b/pyproject.toml index 59d5b2bbbc0..c147ab1c9a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ directory = "test_report" title = "Lambda Powertools Test Coverage" [tool.coverage.report] +fail_under = 90 exclude_lines = [ # Have to re-enable the standard pragma "pragma: no cover", diff --git a/tests/functional/test_logger.py b/tests/functional/test_logger.py index 6b6a4bb6dde..211f12f8fc1 100644 --- a/tests/functional/test_logger.py +++ b/tests/functional/test_logger.py @@ -7,7 +7,7 @@ from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.logging.exceptions import InvalidLoggerSamplingRateError -from aws_lambda_powertools.logging.logger import JsonFormatter, set_package_logger +from aws_lambda_powertools.logging.logger import set_package_logger @pytest.fixture @@ -39,227 +39,220 @@ def lambda_context(): return namedtuple("LambdaContext", lambda_context.keys())(*lambda_context.values()) +@pytest.fixture +def lambda_event(): + return {"greeting": "hello"} + + +def capture_logging_output(stdout): + return json.loads(stdout.getvalue()) + + +def capture_multiple_logging_statements_output(stdout): + return [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] + + def test_setup_service_name(root_logger, stdout): - # GIVEN service is explicitly defined - # WHEN logger is setup - # THEN service field should be equals service given service_name = "payment" + # GIVEN Logger is initialized + # WHEN service is explicitly defined logger = Logger(service=service_name, stream=stdout) logger.info("Hello") - log = json.loads(stdout.getvalue()) + # THEN service field should be equals service given + log = capture_logging_output(stdout) assert service_name == log["service"] def test_setup_no_service_name(stdout): - # GIVEN no service is explicitly defined - # WHEN logger is setup - # THEN service field should be "service_undefined" + # GIVEN Logger is initialized + # WHEN no service is explicitly defined logger = Logger(stream=stdout) + logger.info("Hello") - log = json.loads(stdout.getvalue()) + # THEN service field should be "service_undefined" + log = capture_logging_output(stdout) assert "service_undefined" == log["service"] def test_setup_service_env_var(monkeypatch, stdout): - # GIVEN service is explicitly defined via POWERTOOLS_SERVICE_NAME env - # WHEN logger is setup - # THEN service field should be equals POWERTOOLS_SERVICE_NAME value service_name = "payment" + # GIVEN Logger is initialized + # WHEN service is explicitly defined via POWERTOOLS_SERVICE_NAME env monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", service_name) - logger = Logger(stream=stdout) + logger.info("Hello") - log = json.loads(stdout.getvalue()) + # THEN service field should be equals POWERTOOLS_SERVICE_NAME value + log = capture_logging_output(stdout) assert service_name == log["service"] -def test_setup_sampling_rate(monkeypatch, stdout): - # GIVEN samping rate is explicitly defined via POWERTOOLS_LOGGER_SAMPLE_RATE env - # WHEN logger is setup - # THEN sampling rate should be equals POWERTOOLS_LOGGER_SAMPLE_RATE value and should sample debug logs - +def test_setup_sampling_rate_env_var(monkeypatch, stdout): + # GIVEN Logger is initialized + # WHEN samping rate is explicitly set to 100% via POWERTOOLS_LOGGER_SAMPLE_RATE env sampling_rate = "1" monkeypatch.setenv("POWERTOOLS_LOGGER_SAMPLE_RATE", sampling_rate) - monkeypatch.setenv("LOG_LEVEL", "INFO") - - logger = Logger(stream=stdout) + logger = Logger(stream=stdout, level="INFO") logger.debug("I am being sampled") - log = json.loads(stdout.getvalue()) + # THEN sampling rate should be equals POWERTOOLS_LOGGER_SAMPLE_RATE value + # log level should be DEBUG + # and debug log statements should be in stdout + log = capture_logging_output(stdout) assert sampling_rate == log["sampling_rate"] assert "DEBUG" == log["level"] assert "I am being sampled" == log["message"] def test_inject_lambda_context(lambda_context, stdout): - # GIVEN a lambda function is decorated with logger - # WHEN logger is setup - # THEN lambda contextual info should always be in the logs - logger_context_keys = ( - "function_name", - "function_memory_size", - "function_arn", - "function_request_id", - ) - + # GIVEN Logger is initialized logger = Logger(stream=stdout) + # WHEN a lambda function is decorated with logger @logger.inject_lambda_context def handler(event, context): logger.info("Hello") handler({}, lambda_context) - log = json.loads(stdout.getvalue()) - - for key in logger_context_keys: + # THEN lambda contextual info should always be in the logs + log = capture_logging_output(stdout) + expected_logger_context_keys = ( + "function_name", + "function_memory_size", + "function_arn", + "function_request_id", + ) + for key in expected_logger_context_keys: assert key in log -def test_inject_lambda_context_log_event_request(lambda_context, stdout): - # GIVEN a lambda function is decorated with logger instructed to log event - # WHEN logger is setup - # THEN logger should log event received from Lambda - lambda_event = {"greeting": "hello"} - +def test_inject_lambda_context_log_event_request(lambda_context, stdout, lambda_event): + # GIVEN Logger is initialized logger = Logger(stream=stdout) + # WHEN a lambda function is decorated with logger instructed to log event @logger.inject_lambda_context(log_event=True) - # @logger.inject_lambda_context(log_event=True) def handler(event, context): logger.info("Hello") handler(lambda_event, lambda_context) - # Given that our string buffer has many log statements separated by newline \n - # We need to clean it before we can assert on - logs = [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] - logged_event, _ = logs - assert "greeting" in logged_event["message"] + # THEN logger should log event received from Lambda + logged_event, _ = capture_multiple_logging_statements_output(stdout) + assert logged_event["message"] == lambda_event -def test_inject_lambda_context_log_event_request_env_var(monkeypatch, lambda_context, stdout): - # GIVEN a lambda function is decorated with logger instructed to log event - # via POWERTOOLS_LOGGER_LOG_EVENT env - # WHEN logger is setup - # THEN logger should log event received from Lambda - lambda_event = {"greeting": "hello"} +def test_inject_lambda_context_log_event_request_env_var(monkeypatch, lambda_context, stdout, lambda_event): + # GIVEN Logger is initialized monkeypatch.setenv("POWERTOOLS_LOGGER_LOG_EVENT", "true") - logger = Logger(stream=stdout) + # WHEN a lambda function is decorated with logger instructed to log event + # via POWERTOOLS_LOGGER_LOG_EVENT env @logger.inject_lambda_context def handler(event, context): logger.info("Hello") handler(lambda_event, lambda_context) - # Given that our string buffer has many log statements separated by newline \n - # We need to clean it before we can assert on - logs = [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] - - event = {} - for log in logs: - if "greeting" in log["message"]: - event = log["message"] - - assert event == lambda_event - + # THEN logger should log event received from Lambda + logged_event, _ = capture_multiple_logging_statements_output(stdout) + assert logged_event["message"] == lambda_event -def test_inject_lambda_context_log_no_request_by_default(monkeypatch, lambda_context, stdout): - # GIVEN a lambda function is decorated with logger - # WHEN logger is setup - # THEN logger should not log event received by lambda handler - lambda_event = {"greeting": "hello"} +def test_inject_lambda_context_log_no_request_by_default(monkeypatch, lambda_context, stdout, lambda_event): + # GIVEN Logger is initialized logger = Logger(stream=stdout) + # WHEN a lambda function is decorated with logger @logger.inject_lambda_context def handler(event, context): logger.info("Hello") handler(lambda_event, lambda_context) - # Given that our string buffer has many log statements separated by newline \n - # We need to clean it before we can assert on - logs = [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] - - event = {} - for log in logs: - if "greeting" in log["message"]: - event = log["message"] - - assert event != lambda_event + # THEN logger should not log event received by lambda handler + log = capture_logging_output(stdout) + assert log["message"] != lambda_event def test_inject_lambda_cold_start(lambda_context, stdout): - # GIVEN a lambda function is decorated with logger, and called twice - # WHEN logger is setup - # THEN cold_start key should only be true in the first call - + # cold_start can be false as it's a global variable in Logger module + # so we reset it to simulate the correct behaviour + # since Lambda will only import our logger lib once per concurrent execution from aws_lambda_powertools.logging import logger - # # As we run tests in parallel global cold_start value can be false - # # here we reset to simulate the correct behaviour - # # since Lambda will only import our logger lib once per concurrent execution logger.is_cold_start = True + # GIVEN Logger is initialized logger = Logger(stream=stdout) - def custom_method(): - logger.info("Hello from method") - + # WHEN a lambda function is decorated with logger, and called twice @logger.inject_lambda_context def handler(event, context): - custom_method() logger.info("Hello") handler({}, lambda_context) handler({}, lambda_context) - # Given that our string buffer has many log statements separated by newline \n - # We need to clean it before we can assert on - logs = [json.loads(line.strip()) for line in stdout.getvalue().split("\n") if line] - first_log, second_log, third_log, fourth_log = logs - - # First execution + # THEN cold_start key should only be true in the first call + first_log, second_log = capture_multiple_logging_statements_output(stdout) assert first_log["cold_start"] is True - assert second_log["cold_start"] is True - - # Second execution - assert third_log["cold_start"] is False - assert fourth_log["cold_start"] is False + assert second_log["cold_start"] is False -def test_package_logger(capsys): +def test_package_logger_stream(stdout): + # GIVEN package logger "aws_lambda_powertools" is explicitly set with no params + set_package_logger(stream=stdout) - set_package_logger() + # WHEN Tracer is initialized in disabled mode Tracer(disabled=True) - output = capsys.readouterr() - assert "Tracing has been disabled" in output.out + # THEN Tracer debug log statement should be logged + output = stdout.getvalue() + logger = logging.getLogger("aws_lambda_powertools") + assert "Tracing has been disabled" in output + assert logger.level == logging.DEBUG -def test_package_logger_format(stdout, capsys): - set_package_logger(stream=stdout, formatter=JsonFormatter(formatter="test")) +def test_package_logger_format(capsys): + # GIVEN package logger "aws_lambda_powertools" is explicitly + # with a custom formatter + formatter = logging.Formatter("message=%(message)s") + set_package_logger(formatter=formatter) + + # WHEN Tracer is initialized in disabled mode Tracer(disabled=True) - output = json.loads(stdout.getvalue().split("\n")[0]) - assert "test" in output["formatter"] + # THEN Tracer debug log statement should be logged using `message=` format + output = capsys.readouterr().out + logger = logging.getLogger("aws_lambda_powertools") + assert "message=" in output + assert logger.level == logging.DEBUG def test_logger_append_duplicated(stdout): + # GIVEN Logger is initialized with request_id field logger = Logger(stream=stdout, request_id="value") + + # WHEN `request_id` is appended to the existing structured log + # using a different value logger.structure_logs(append=True, request_id="new_value") logger.info("log") - log = json.loads(stdout.getvalue()) + + # THEN subsequent log statements should have the latest value + log = capture_logging_output(stdout) assert "new_value" == log["request_id"] def test_logger_invalid_sampling_rate(): + # GIVEN Logger is initialized + # WHEN sampling_rate non-numeric value + # THEN we should raise InvalidLoggerSamplingRateError with pytest.raises(InvalidLoggerSamplingRateError): Logger(sampling_rate="TEST") diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index 244a56119cd..efc93daa739 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -52,17 +52,18 @@ def non_str_dimensions() -> List[Dict[str, Any]]: @pytest.fixture -def namespace() -> Dict[str, str]: +def namespace() -> str: return "test_namespace" @pytest.fixture -def a_hundred_metrics(namespace=namespace) -> List[Dict[str, str]]: - metrics = [] - for i in range(100): - metrics.append({"name": f"metric_{i}", "unit": "Count", "value": 1}) +def service() -> str: + return "test_service" + - return metrics +@pytest.fixture +def a_hundred_metrics(namespace=namespace) -> List[Dict[str, str]]: + return [{"name": f"metric_{i}", "unit": "Count", "value": 1} for i in range(100)] def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: str) -> Dict: @@ -92,21 +93,23 @@ def remove_timestamp(metrics: List): del metric["_aws"]["Timestamp"] -def test_single_metric_one_metric_only(capsys, metric, dimension, namespace): - # GIVEN we attempt to add more than one metric +def capture_metrics_output(capsys): + return json.loads(capsys.readouterr().out.strip()) + + +def test_single_metric_logs_one_metric_only(capsys, metric, dimension, namespace): + # GIVEN we try adding more than one metric # WHEN using single_metric context manager with single_metric(namespace=namespace, **metric) as my_metric: my_metric.add_metric(name="second_metric", unit="Count", value=1) - my_metric.add_metric(name="third_metric", unit="Seconds", value=1) my_metric.add_dimension(**dimension) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - # THEN we should only have the first metric added - assert expected["_aws"] == output["_aws"] + remove_timestamp(metrics=[output, expected]) + assert expected == output def test_log_metrics(capsys, metrics, dimensions, namespace): @@ -121,65 +124,48 @@ def test_log_metrics(capsys, metrics, dimensions, namespace): # and flush all metrics at the end of a function execution @my_metrics.log_metrics def lambda_handler(evt, ctx): - return True + pass lambda_handler({}, {}) - - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) expected = serialize_metrics(metrics=metrics, dimensions=dimensions, namespace=namespace) - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - # THEN we should have no exceptions - # and a valid EMF object should've been flushed correctly - assert expected["_aws"] == output["_aws"] - for dimension in dimensions: - assert dimension["name"] in output + # and a valid EMF object should be flushed correctly + remove_timestamp(metrics=[output, expected]) + assert expected == output def test_namespace_env_var(monkeypatch, capsys, metric, dimension, namespace): - # GIVEN we use POWERTOOLS_METRICS_NAMESPACE + # GIVEN POWERTOOLS_METRICS_NAMESPACE is set monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace) - # WHEN creating a metric but don't explicitly - # add a namespace - with single_metric(**metric) as my_metrics: - my_metrics.add_dimension(**dimension) - monkeypatch.delenv("POWERTOOLS_METRICS_NAMESPACE") + # WHEN creating a metric without explicitly adding a namespace + with single_metric(**metric) as my_metric: + my_metric.add_dimension(**dimension) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - - # THEN we should add a namespace implicitly - # with the value of POWERTOOLS_METRICS_NAMESPACE env var - assert expected["_aws"] == output["_aws"] + # THEN we should add a namespace using POWERTOOLS_METRICS_NAMESPACE env var value + remove_timestamp(metrics=[output, expected]) + assert expected == output def test_service_env_var(monkeypatch, capsys, metric, namespace): # GIVEN we use POWERTOOLS_SERVICE_NAME monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", "test_service") - my_metrics = Metrics(namespace=namespace) - - # WHEN creating a metric but don't explicitly - # add a dimension - @my_metrics.log_metrics - def lambda_handler(evt, context): - my_metrics.add_metric(**metric) - return True - lambda_handler({}, {}) - - monkeypatch.delenv("POWERTOOLS_SERVICE_NAME") + # WHEN creating a metric without explicitly adding a dimension + with single_metric(**metric, namespace=namespace): + pass - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) expected_dimension = {"name": "service", "value": "test_service"} expected = serialize_single_metric(metric=metric, dimension=expected_dimension, namespace=namespace) - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - - # THEN metrics should be logged using the implicitly created "service" dimension + # THEN a metric should be logged using the implicitly created "service" dimension + remove_timestamp(metrics=[output, expected]) assert expected == output @@ -194,7 +180,7 @@ def test_metrics_spillover(monkeypatch, capsys, metric, dimension, namespace, a_ # THEN it should serialize and flush all metrics at the 100th # and clear all metrics and dimensions from memory - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) spillover_metrics = output["_aws"]["CloudWatchMetrics"][0]["Metrics"] assert my_metrics.metric_set == {} assert len(spillover_metrics) == 100 @@ -206,87 +192,75 @@ def test_metrics_spillover(monkeypatch, capsys, metric, dimension, namespace, a_ # THEN serializing the 101th metric should # create a new EMF object with a single metric in it (101th) - # and contain have the same dimension we previously added + # and contain the same dimension we previously added serialized_101th_metric = my_metrics.serialize_metric_set() expected_101th_metric = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) remove_timestamp(metrics=[serialized_101th_metric, expected_101th_metric]) + assert serialized_101th_metric == expected_101th_metric - assert serialized_101th_metric["_aws"] == expected_101th_metric["_aws"] - -def test_log_metrics_should_invoke_function(metric, dimension, namespace): +def test_log_metrics_decorator_call_decorated_function(metric, namespace, service): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace) + my_metrics = Metrics(service=service, namespace=namespace) # WHEN log_metrics is used to serialize metrics @my_metrics.log_metrics def lambda_handler(evt, context): - my_metrics.add_metric(**metric) - my_metrics.add_dimension(**dimension) return True # THEN log_metrics should invoke the function it decorates - # and return no error if we have a metric, namespace, and a dimension - lambda_handler({}, {}) + # and return no error if we have a namespace and dimension + assert lambda_handler({}, {}) is True -def test_incorrect_metric_unit(metric, dimension, namespace): - # GIVEN we pass a metric unit not supported by CloudWatch +def test_schema_validation_incorrect_metric_unit(metric, dimension, namespace): + # GIVEN we pass a metric unit that is not supported by CloudWatch metric["unit"] = "incorrect_unit" - # WHEN we attempt to add a new metric - # THEN it should fail validation and raise MetricUnitError + # WHEN we try adding a new metric + # THEN it should fail metric unit validation with pytest.raises(MetricUnitError): with single_metric(**metric) as my_metric: my_metric.add_dimension(**dimension) -def test_schema_no_namespace(metric, dimension): - # GIVEN we add any metric or dimension - # but no namespace - +def test_schema_validation_no_namespace(metric, dimension): + # GIVEN we don't add any namespace # WHEN we attempt to serialize a valid EMF object - # THEN it should fail validation and raise SchemaValidationError - with pytest.raises(SchemaValidationError): - with single_metric(**metric) as my_metric: - my_metric.add_dimension(**dimension) + # THEN it should fail namespace validation + with pytest.raises(SchemaValidationError, match=".*Namespace must be string"): + with single_metric(**metric): + pass -def test_schema_incorrect_value(metric, dimension, namespace): - # GIVEN we pass an incorrect metric value (non-number/float) +def test_schema_validation_incorrect_metric_value(metric, dimension, namespace): + # GIVEN we pass an incorrect metric value (non-numeric) metric["value"] = "some_value" # WHEN we attempt to serialize a valid EMF object # THEN it should fail validation and raise SchemaValidationError with pytest.raises(MetricValueError): - with single_metric(**metric) as my_metric: - my_metric.add_dimension(**dimension) + with single_metric(**metric): + pass -def test_schema_no_metrics(dimensions, namespace): +def test_schema_no_metrics(service, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace) - - # WHEN no metrics have been added - # but a namespace and dimensions only - for dimension in dimensions: - my_metrics.add_dimension(**dimension) + my_metrics = Metrics(service=service, namespace=namespace) # THEN it should fail validation and raise SchemaValidationError - with pytest.raises(SchemaValidationError): + with pytest.raises(SchemaValidationError, match=".*Metrics must contain at least 1 items"): my_metrics.serialize_metric_set() def test_exceed_number_of_dimensions(metric, namespace): # GIVEN we we have more dimensions than CloudWatch supports - dimensions = [] - for i in range(11): - dimensions.append({"name": f"test_{i}", "value": "test"}) + dimensions = [{"name": f"test_{i}", "value": "test"} for i in range(11)] # WHEN we attempt to serialize them into a valid EMF object # THEN it should fail validation and raise SchemaValidationError - with pytest.raises(SchemaValidationError): - with single_metric(**metric) as my_metric: + with pytest.raises(SchemaValidationError, match="must contain less than or equal to 9 items"): + with single_metric(**metric, namespace=namespace) as my_metric: for dimension in dimensions: my_metric.add_dimension(**dimension) @@ -294,9 +268,8 @@ def test_exceed_number_of_dimensions(metric, namespace): def test_log_metrics_during_exception(capsys, metric, dimension, namespace): # GIVEN Metrics is initialized my_metrics = Metrics(namespace=namespace) - - my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) + my_metrics.add_metric(**metric) # WHEN log_metrics is used to serialize metrics # but an error has been raised during handler execution @@ -307,31 +280,30 @@ def lambda_handler(evt, context): with pytest.raises(ValueError): lambda_handler({}, {}) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - # THEN we should log metrics and propagate the exception up - assert expected["_aws"] == output["_aws"] + # THEN we should log metrics either way + remove_timestamp(metrics=[output, expected]) + assert expected == output def test_log_metrics_raise_on_empty_metrics(capsys, metric, dimension, namespace): # GIVEN Metrics is initialized my_metrics = Metrics(service="test_service", namespace=namespace) + # WHEN log_metrics is used with raise_on_empty_metrics param and has no metrics @my_metrics.log_metrics(raise_on_empty_metrics=True) def lambda_handler(evt, context): - # WHEN log_metrics is used with raise_on_empty_metrics param and has no metrics - return True + pass # THEN the raised exception should be SchemaValidationError # and specifically about the lack of Metrics - with pytest.raises(SchemaValidationError, match="_aws\.CloudWatchMetrics\[0\]\.Metrics"): # noqa: W605 + with pytest.raises(SchemaValidationError, match=".*Metrics must contain at least 1 items"): lambda_handler({}, {}) def test_all_possible_metric_units(metric, dimension, namespace): - # GIVEN we add a metric for each metric unit supported by CloudWatch # where metric unit as MetricUnit key e.g. "Seconds", "BytesPerSecond" for unit in MetricUnit: @@ -344,9 +316,8 @@ def test_all_possible_metric_units(metric, dimension, namespace): # WHEN we iterate over all available metric unit keys from MetricUnit enum all_metric_units = [unit.value for unit in MetricUnit] - # metric unit as MetricUnit value e.g. "Seconds", "Bytes/Second" for unit in all_metric_units: - metric["unit"] = unit + metric["unit"] = unit # e.g. "Seconds", "Bytes/Second" # THEN we raise no MetricUnitError nor SchemaValidationError with single_metric(namespace=namespace, **metric) as my_metric: my_metric.add_dimension(**dimension) @@ -364,17 +335,15 @@ def test_metrics_reuse_metric_set(metric, dimension, namespace): assert my_metrics_2.metric_set == my_metrics.metric_set -def test_log_metrics_clear_metrics_after_invocation(metric, dimension, namespace): +def test_log_metrics_clear_metrics_after_invocation(metric, service, namespace): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace) - + my_metrics = Metrics(service=service, namespace=namespace) my_metrics.add_metric(**metric) - my_metrics.add_dimension(**dimension) # WHEN log_metrics is used to flush metrics from memory @my_metrics.log_metrics def lambda_handler(evt, context): - return True + pass lambda_handler({}, {}) @@ -382,11 +351,10 @@ def lambda_handler(evt, context): assert my_metrics.metric_set == {} -def test_log_metrics_non_string_dimension_values(capsys, metrics, non_str_dimensions, namespace): +def test_log_metrics_non_string_dimension_values(capsys, service, metric, non_str_dimensions, namespace): # GIVEN Metrics is initialized and dimensions with non-string values are added - my_metrics = Metrics(namespace=namespace) - for metric in metrics: - my_metrics.add_metric(**metric) + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) for dimension in non_str_dimensions: my_metrics.add_dimension(**dimension) @@ -394,179 +362,137 @@ def test_log_metrics_non_string_dimension_values(capsys, metrics, non_str_dimens # and flush all metrics at the end of a function execution @my_metrics.log_metrics def lambda_handler(evt, ctx): - return True + pass lambda_handler({}, {}) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) # THEN we should have no exceptions - # and dimension values hould be serialized as strings + # and dimension values should be serialized as strings for dimension in non_str_dimensions: assert isinstance(output[dimension["name"]], str) -def test_log_metrics_with_explicit_namespace(capsys, metrics, dimensions, namespace): - # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace=namespace) - for metric in metrics: - my_metrics.add_metric(**metric) - for dimension in dimensions: - my_metrics.add_dimension(**dimension) +def test_log_metrics_with_explicit_namespace(capsys, metric, service, namespace): + # GIVEN Metrics is initialized with explicit namespace + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) # WHEN we utilize log_metrics to serialize # and flush all metrics at the end of a function execution @my_metrics.log_metrics def lambda_handler(evt, ctx): - return True + pass lambda_handler({}, {}) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) - dimensions.append({"name": "service", "value": "test_service"}) - expected = serialize_metrics(metrics=metrics, dimensions=dimensions, namespace=namespace) + # THEN we should have no exceptions and the namespace should be set + # using the service value passed to Metrics constructor + assert namespace == output["_aws"]["CloudWatchMetrics"][0]["Namespace"] - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - # THEN we should have no exceptions and the namespace should be set to the name provided in the - # service passed to Metrics constructor - assert expected == output - - -def test_log_metrics_with_implicit_dimensions(capsys, metrics, namespace): +def test_log_metrics_with_implicit_dimensions(capsys, metric, namespace, service): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace=namespace) - for metric in metrics: - my_metrics.add_metric(**metric) + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) # WHEN we utilize log_metrics to serialize and don't explicitly add any dimensions @my_metrics.log_metrics def lambda_handler(evt, ctx): - return True + pass lambda_handler({}, {}) - output = json.loads(capsys.readouterr().out.strip()) - - expected_dimensions = [{"name": "service", "value": "test_service"}] - expected = serialize_metrics(metrics=metrics, dimensions=expected_dimensions, namespace=namespace) - - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different + output = capture_metrics_output(capsys) # THEN we should have no exceptions and the dimensions should be set to the name provided in the # service passed to Metrics constructor - assert expected == output + assert service == output["service"] -def test_log_metrics_with_renamed_service(capsys, metrics, metric): +def test_log_metrics_with_renamed_service(capsys, metric, service): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace="test_application") - for metric in metrics: - my_metrics.add_metric(**metric) + my_metrics = Metrics(service=service, namespace="test_application") + another_service_dimension = {"name": "service", "value": "another_test_service"} @my_metrics.log_metrics def lambda_handler(evt, ctx): # WHEN we manually call add_dimension to change the value of the service dimension - my_metrics.add_dimension(name="service", value="another_test_service") + my_metrics.add_dimension(**another_service_dimension) my_metrics.add_metric(**metric) - return True lambda_handler({}, {}) + output = capture_metrics_output(capsys) - output = json.loads(capsys.readouterr().out.strip()) lambda_handler({}, {}) - second_output = json.loads(capsys.readouterr().out.strip()) - - remove_timestamp(metrics=[output]) # Timestamp will always be different + second_output = capture_metrics_output(capsys) # THEN we should have no exceptions and the dimensions should be set to the name provided in the # add_dimension call - assert output["service"] == "another_test_service" - assert second_output["service"] == "another_test_service" - - -def test_single_metric_with_service(capsys, metric, dimension, namespace): - # GIVEN we pass namespace parameter to single_metric - - # WHEN creating a metric - with single_metric(**metric, namespace=namespace) as my_metrics: - my_metrics.add_dimension(**dimension) - - output = json.loads(capsys.readouterr().out.strip()) - expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) - - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different - - # THEN namespace should match value passed as service - assert expected["_aws"] == output["_aws"] + assert output["service"] == another_service_dimension["value"] + assert second_output["service"] == another_service_dimension["value"] def test_namespace_var_precedence(monkeypatch, capsys, metric, dimension, namespace): # GIVEN we use POWERTOOLS_METRICS_NAMESPACE - monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", namespace) + monkeypatch.setenv("POWERTOOLS_METRICS_NAMESPACE", "a_namespace") # WHEN creating a metric and explicitly set a namespace with single_metric(namespace=namespace, **metric) as my_metrics: my_metrics.add_dimension(**dimension) - monkeypatch.delenv("POWERTOOLS_METRICS_NAMESPACE") - output = json.loads(capsys.readouterr().out.strip()) - expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace) - - remove_timestamp(metrics=[output, expected]) # Timestamp will always be different + output = capture_metrics_output(capsys) # THEN namespace should match the explicitly passed variable and not the env var - assert expected["_aws"] == output["_aws"] + assert namespace == output["_aws"]["CloudWatchMetrics"][0]["Namespace"] -def test_emit_cold_start_metric(capsys, namespace): +def test_log_metrics_capture_cold_start_metric(capsys, namespace, service): # GIVEN Metrics is initialized - my_metrics = Metrics(service="test_service", namespace=namespace) + my_metrics = Metrics(service=service, namespace=namespace) # WHEN log_metrics is used with capture_cold_start_metric @my_metrics.log_metrics(capture_cold_start_metric=True) def lambda_handler(evt, context): - return True + pass LambdaContext = namedtuple("LambdaContext", "function_name") lambda_handler({}, LambdaContext("example_fn")) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) # THEN ColdStart metric and function_name dimension should be logged assert output["ColdStart"] == 1 assert output["function_name"] == "example_fn" -def test_emit_cold_start_metric_only_once(capsys, namespace, dimension, metric): +def test_emit_cold_start_metric_only_once(capsys, namespace, service, metric): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace) + my_metrics = Metrics(service=service, namespace=namespace) # WHEN log_metrics is used with capture_cold_start_metric # and handler is called more than once @my_metrics.log_metrics(capture_cold_start_metric=True) def lambda_handler(evt, context): my_metrics.add_metric(**metric) - my_metrics.add_dimension(**dimension) LambdaContext = namedtuple("LambdaContext", "function_name") lambda_handler({}, LambdaContext("example_fn")) - capsys.readouterr().out.strip() + _ = capture_metrics_output(capsys) # ignore first stdout captured - # THEN ColdStart metric and function_name dimension should be logged - # only once + # THEN ColdStart metric and function_name dimension should be logged once lambda_handler({}, LambdaContext("example_fn")) - - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) assert "ColdStart" not in output - assert "function_name" not in output -def test_log_metrics_decorator_no_metrics(dimensions, namespace): +def test_log_metrics_decorator_no_metrics_warning(dimensions, namespace, service): # GIVEN Metrics is initialized - my_metrics = Metrics(namespace=namespace, service="test_service") + my_metrics = Metrics(namespace=namespace, service=service) # WHEN using the log_metrics decorator and no metrics have been added @my_metrics.log_metrics @@ -580,28 +506,22 @@ def lambda_handler(evt, context): assert str(w[-1].message) == "No metrics to publish, skipping" -def test_log_metrics_with_implicit_dimensions_called_twice(capsys, metrics, namespace): +def test_log_metrics_with_implicit_dimensions_called_twice(capsys, metric, namespace, service): # GIVEN Metrics is initialized with service specified - my_metrics = Metrics(service="test_service", namespace=namespace) + my_metrics = Metrics(service=service, namespace=namespace) # WHEN we utilize log_metrics to serialize and don't explicitly add any dimensions, # and the lambda function is called more than once @my_metrics.log_metrics def lambda_handler(evt, ctx): - for metric in metrics: - my_metrics.add_metric(**metric) + my_metrics.add_metric(**metric) return True lambda_handler({}, {}) - output = json.loads(capsys.readouterr().out.strip()) + output = capture_metrics_output(capsys) lambda_handler({}, {}) - second_output = json.loads(capsys.readouterr().out.strip()) - - expected_dimensions = [{"name": "service", "value": "test_service"}] - expected = serialize_metrics(metrics=metrics, dimensions=expected_dimensions, namespace=namespace) - - remove_timestamp(metrics=[output, expected, second_output]) # Timestamp will always be different + second_output = capture_metrics_output(capsys) # THEN we should have no exceptions and the dimensions should be set to the name provided in the # service passed to Metrics constructor diff --git a/tests/functional/test_tracing.py b/tests/functional/test_tracing.py index 6cff182015e..cda0a85cc4d 100644 --- a/tests/functional/test_tracing.py +++ b/tests/functional/test_tracing.py @@ -14,120 +14,125 @@ def reset_tracing_config(): yield +@pytest.fixture +def service_name(): + return "booking" + + def test_capture_lambda_handler(dummy_response): - # GIVEN tracer is disabled, and decorator is used - # WHEN a lambda handler is run - # THEN tracer should not raise an Exception + # GIVEN tracer lambda handler decorator is used tracer = Tracer(disabled=True) + # WHEN a lambda handler is run @tracer.capture_lambda_handler def handler(event, context): return dummy_response + # THEN tracer should not raise an Exception handler({}, {}) def test_capture_method(dummy_response): - # GIVEN tracer is disabled, and method decorator is used - # WHEN a function is run - # THEN tracer should not raise an Exception - + # GIVEN tracer method decorator is used tracer = Tracer(disabled=True) + # WHEN a function is run @tracer.capture_method def greeting(name, message): return dummy_response + # THEN tracer should not raise an Exception greeting(name="Foo", message="Bar") def test_tracer_lambda_emulator(monkeypatch, dummy_response): - # GIVEN tracer is run locally - # WHEN a lambda function is run through SAM CLI - # THEN tracer should not raise an Exception + # GIVEN tracer runs locally monkeypatch.setenv("AWS_SAM_LOCAL", "true") tracer = Tracer() + # WHEN a lambda function is run through SAM CLI @tracer.capture_lambda_handler def handler(event, context): return dummy_response + # THEN tracer should run in disabled mode, and not raise an Exception handler({}, {}) - monkeypatch.delenv("AWS_SAM_LOCAL") def test_tracer_metadata_disabled(dummy_response): # GIVEN tracer is disabled, and annotations/metadata are used - # WHEN a lambda handler is run - # THEN tracer should not raise an Exception and simply ignore tracer = Tracer(disabled=True) + # WHEN a lambda handler is run @tracer.capture_lambda_handler def handler(event, context): tracer.put_annotation("PaymentStatus", "SUCCESS") tracer.put_metadata("PaymentMetadata", "Metadata") return dummy_response + # THEN tracer should not raise any Exception handler({}, {}) -def test_tracer_env_vars(monkeypatch): - # GIVEN tracer disabled, is run without parameters - # WHEN service is explicitly defined - # THEN tracer should have use that service name - service_name = "booking" +def test_tracer_service_env_var(monkeypatch, service_name): + # GIVEN tracer is run without parameters + # WHEN service is implicitly defined via env var monkeypatch.setenv("POWERTOOLS_SERVICE_NAME", service_name) - tracer_env_var = Tracer(disabled=True) + tracer = Tracer(disabled=True) + + # THEN tracer should have use that service name + assert tracer.service == service_name - assert tracer_env_var.service == service_name +def test_tracer_explicit_service(monkeypatch, service_name): + # GIVEN tracer is disabled + # WHEN service is explicitly defined tracer_explicit = Tracer(disabled=True, service=service_name) assert tracer_explicit.service == service_name monkeypatch.setenv("POWERTOOLS_TRACE_DISABLED", "true") tracer = Tracer() - assert bool(tracer.disabled) is True + # THEN tracer should have use that service name + assert tracer.service == service_name -def test_tracer_with_exception(mocker): - # GIVEN tracer is disabled, decorator is used - # WHEN a lambda handler or method returns an Exception - # THEN tracer should reraise the same Exception +def test_tracer_propagate_exception(mocker): + # GIVEN tracer decorator is used class CustomException(Exception): pass tracer = Tracer(disabled=True) + # WHEN a lambda handler or method returns an Exception @tracer.capture_lambda_handler def handler(event, context): raise CustomException("test") @tracer.capture_method - def greeting(name, message): + def greeting(): raise CustomException("test") + # THEN tracer should reraise the same Exception with pytest.raises(CustomException): handler({}, {}) with pytest.raises(CustomException): - greeting(name="Foo", message="Bar") + greeting() -def test_tracer_reuse(): - # GIVEN tracer A, B were initialized - # WHEN tracer B explicitly reuses A config - # THEN tracer B attributes should be equal to tracer A - service_name = "booking" +def test_tracer_reuse_configuration(service_name): + # GIVEN tracer A is initialized tracer_a = Tracer(disabled=True, service=service_name) + # WHEN tracer B is initialized afterwards tracer_b = Tracer() - assert id(tracer_a) != id(tracer_b) + # THEN tracer B attributes should be equal to tracer A assert tracer_a.__dict__.items() == tracer_b.__dict__.items() def test_tracer_method_nested_sync(mocker): - # GIVEN tracer is disabled, decorator is used + # GIVEN tracer decorator is used # WHEN multiple sync functions are nested # THEN tracer should not raise a Runtime Error tracer = Tracer(disabled=True) diff --git a/tests/unit/test_tracing.py b/tests/unit/test_tracing.py index 65cb04c997b..d1e5408bb77 100644 --- a/tests/unit/test_tracing.py +++ b/tests/unit/test_tracing.py @@ -79,15 +79,20 @@ class In_subsegment(NamedTuple): def test_tracer_lambda_handler(mocker, dummy_response, provider_stub, in_subsegment_mock): + # GIVEN Tracer is initialized with booking as the service name provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, service="booking") + # WHEN lambda_handler decorator is used @tracer.capture_lambda_handler def handler(event, context): return dummy_response handler({}, mocker.MagicMock()) + # THEN we should have a subsegment named handler + # annotate cold start, and add its response as trace metadata + # and use service name as a metadata namespace assert in_subsegment_mock.in_subsegment.call_count == 1 assert in_subsegment_mock.in_subsegment.call_args == mocker.call(name="## handler") assert in_subsegment_mock.put_metadata.call_args == mocker.call( @@ -98,19 +103,39 @@ def handler(event, context): def test_tracer_method(mocker, dummy_response, provider_stub, in_subsegment_mock): + # GIVEN Tracer is initialized with booking as the service name provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) - Tracer(provider=provider, service="booking") + tracer = Tracer(provider=provider, service="booking") + + # WHEN capture_method decorator is used + @tracer.capture_method + def greeting(name, message): + return dummy_response + + greeting(name="Foo", message="Bar") + + # THEN we should have a subsegment named after the method name + # and add its response as trace metadata + # and use service name as a metadata namespace + assert in_subsegment_mock.in_subsegment.call_count == 1 + assert in_subsegment_mock.in_subsegment.call_args == mocker.call(name="## greeting") + assert in_subsegment_mock.put_metadata.call_args == mocker.call( + key="greeting response", value=dummy_response, namespace="booking" + ) def test_tracer_custom_metadata(mocker, dummy_response, provider_stub): + # GIVEN Tracer is initialized with booking as the service name put_metadata_mock = mocker.MagicMock() - annotation_key = "Booking response" - annotation_value = {"bookingStatus": "CONFIRMED"} - provider = provider_stub(put_metadata_mock=put_metadata_mock) tracer = Tracer(provider=provider, service="booking") + + # WHEN put_metadata is used + annotation_key = "Booking response" + annotation_value = {"bookingStatus": "CONFIRMED"} tracer.put_metadata(annotation_key, annotation_value) + # THEN we should have metadata expected and booking as namespace assert put_metadata_mock.call_count == 1 assert put_metadata_mock.call_args_list[0] == mocker.call( key=annotation_key, value=annotation_value, namespace="booking" @@ -118,87 +143,97 @@ def test_tracer_custom_metadata(mocker, dummy_response, provider_stub): def test_tracer_custom_annotation(mocker, dummy_response, provider_stub): + # GIVEN Tracer is initialized put_annotation_mock = mocker.MagicMock() - annotation_key = "BookingId" - annotation_value = "123456" - provider = provider_stub(put_annotation_mock=put_annotation_mock) - tracer = Tracer(provider=provider, service="booking") + tracer = Tracer(provider=provider) + # WHEN put_metadata is used + annotation_key = "BookingId" + annotation_value = "123456" tracer.put_annotation(annotation_key, annotation_value) + # THEN we should have an annotation as expected assert put_annotation_mock.call_count == 1 assert put_annotation_mock.call_args == mocker.call(key=annotation_key, value=annotation_value) @mock.patch("aws_lambda_powertools.tracing.Tracer.patch") def test_tracer_autopatch(patch_mock): - # GIVEN tracer is instantiated - # WHEN default options were used, or patch() was called - # THEN tracer should patch all modules + # GIVEN tracer is initialized + # WHEN auto_patch hasn't been explicitly disabled Tracer(disabled=True) + + # THEN tracer should patch all modules assert patch_mock.call_count == 1 @mock.patch("aws_lambda_powertools.tracing.Tracer.patch") def test_tracer_no_autopatch(patch_mock): - # GIVEN tracer is instantiated + # GIVEN tracer is initialized # WHEN auto_patch is disabled - # THEN tracer should not patch any module Tracer(disabled=True, auto_patch=False) + + # THEN tracer should not patch any module assert patch_mock.call_count == 0 -def test_tracer_lambda_handler_empty_response_metadata(mocker, provider_stub): +def test_tracer_lambda_handler_does_not_add_empty_response_as_metadata(mocker, provider_stub): + # GIVEN tracer is initialized put_metadata_mock = mocker.MagicMock() provider = provider_stub(put_metadata_mock=put_metadata_mock) tracer = Tracer(provider=provider) + # WHEN capture_lambda_handler decorator is used + # and the handler response is empty @tracer.capture_lambda_handler def handler(event, context): return handler({}, mocker.MagicMock()) + # THEN we should not add empty metadata assert put_metadata_mock.call_count == 0 -def test_tracer_method_empty_response_metadata(mocker, provider_stub): +def test_tracer_method_does_not_add_empty_response_as_metadata(mocker, provider_stub): + # GIVEN tracer is initialized put_metadata_mock = mocker.MagicMock() provider = provider_stub(put_metadata_mock=put_metadata_mock) tracer = Tracer(provider=provider) + # WHEN capture_method decorator is used + # and the method response is empty @tracer.capture_method def greeting(name, message): return greeting(name="Foo", message="Bar") + # THEN we should not add empty metadata assert put_metadata_mock.call_count == 0 @mock.patch("aws_lambda_powertools.tracing.tracer.aws_xray_sdk.core.patch") -@mock.patch("aws_lambda_powertools.tracing.tracer.aws_xray_sdk.core.patch_all") -def test_tracer_patch(xray_patch_all_mock, xray_patch_mock, mocker): - # GIVEN tracer is instantiated - # WHEN default X-Ray provider client is mocked - # THEN tracer should run just fine - - Tracer() - assert xray_patch_all_mock.call_count == 1 - +def test_tracer_patch_modules(xray_patch_mock, mocker): + # GIVEN tracer is initialized with a list of modules to patch modules = ["boto3"] + + # WHEN modules are supported by X-Ray Tracer(service="booking", patch_modules=modules) + # THEN tracer should run just fine assert xray_patch_mock.call_count == 1 assert xray_patch_mock.call_args == mocker.call(modules) def test_tracer_method_exception_metadata(mocker, provider_stub, in_subsegment_mock): - + # GIVEN tracer is initialized provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, service="booking") + # WHEN capture_method decorator is used + # and the method raises an exception @tracer.capture_method def greeting(name, message): raise ValueError("test") @@ -206,16 +241,20 @@ def greeting(name, message): with pytest.raises(ValueError): greeting(name="Foo", message="Bar") + # THEN we should add the exception using method name as key plus error + # and their service name as the namespace put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1] assert put_metadata_mock_args["key"] == "greeting error" assert put_metadata_mock_args["namespace"] == "booking" def test_tracer_lambda_handler_exception_metadata(mocker, provider_stub, in_subsegment_mock): - + # GIVEN tracer is initialized provider = provider_stub(in_subsegment=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, service="booking") + # WHEN capture_lambda_handler decorator is used + # and the method raises an exception @tracer.capture_lambda_handler def handler(event, context): raise ValueError("test") @@ -223,16 +262,21 @@ def handler(event, context): with pytest.raises(ValueError): handler({}, mocker.MagicMock()) + # THEN we should add the exception using handler name as key plus error + # and their service name as the namespace put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1] - assert put_metadata_mock_args["key"] == "booking error" + assert put_metadata_mock_args["key"] == "handler error" + assert put_metadata_mock_args["namespace"] == "booking" @pytest.mark.asyncio async def test_tracer_method_nested_async(mocker, dummy_response, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized provider = provider_stub(in_subsegment_async=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, service="booking") + # WHEN capture_method decorator is used for nested async methods @tracer.capture_method async def greeting_2(name, message): return dummy_response @@ -250,6 +294,7 @@ async def greeting(name, message): ) = in_subsegment_mock.in_subsegment.call_args_list put_metadata_greeting2_call_args, put_metadata_greeting_call_args = in_subsegment_mock.put_metadata.call_args_list + # THEN we should add metadata for each response like we would for a sync decorated method assert in_subsegment_mock.in_subsegment.call_count == 2 assert in_subsegment_greeting_call_args == mocker.call(name="## greeting") assert in_subsegment_greeting2_call_args == mocker.call(name="## greeting_2") @@ -265,9 +310,10 @@ async def greeting(name, message): @pytest.mark.asyncio async def test_tracer_method_nested_async_disabled(dummy_response): - + # GIVEN tracer is initialized and explicitly disabled tracer = Tracer(service="booking", disabled=True) + # WHEN capture_method decorator is used @tracer.capture_method async def greeting_2(name, message): return dummy_response @@ -277,16 +323,19 @@ async def greeting(name, message): await greeting_2(name, message) return dummy_response + # THEN we should run the decorator methods without side effects ret = await greeting(name="Foo", message="Bar") - assert ret == dummy_response @pytest.mark.asyncio async def test_tracer_method_exception_metadata_async(mocker, provider_stub, in_subsegment_mock): + # GIVEN tracer is initialized provider = provider_stub(in_subsegment_async=in_subsegment_mock.in_subsegment) tracer = Tracer(provider=provider, service="booking") + # WHEN capture_method decorator is used in an async method + # and the method raises an exception @tracer.capture_method async def greeting(name, message): raise ValueError("test") @@ -294,6 +343,8 @@ async def greeting(name, message): with pytest.raises(ValueError): await greeting(name="Foo", message="Bar") + # THEN we should add the exception using method name as key plus error + # and their service name as the namespace put_metadata_mock_args = in_subsegment_mock.put_metadata.call_args[1] assert put_metadata_mock_args["key"] == "greeting error" assert put_metadata_mock_args["namespace"] == "booking" From f5aab395237022c49e3d2956eb84da99373b3892 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Tue, 16 Jun 2020 10:39:02 +0100 Subject: [PATCH 5/8] feat: add metrics metadata (#81) * chore: remove Logger deprecated code * chore: remove Metrics deprecated code * chore: remove models from deprecated code * chore: move logger formatter to its own file * chore: cleanup metrics tests * chore: cleanup tracer tests * chore: cleanup logger tests * chore: cleanup tracer tests * chore: set test coverage to 90% min Signed-off-by: heitorlessa * feat: add metrics metadata #80 * docs: add metrics metadata #80 Signed-off-by: heitorlessa * chore: refactor metric serialization * improv: test metadata with log_metrics * improv: test serialize metric with valid EMF object --- aws_lambda_powertools/metrics/base.py | 85 ++++++++++++++----- aws_lambda_powertools/metrics/metrics.py | 10 ++- docs/content/core/metrics.mdx | 50 +++++++++++ tests/functional/test_metrics.py | 101 ++++++++++++++++++++++- 4 files changed, 224 insertions(+), 22 deletions(-) diff --git a/aws_lambda_powertools/metrics/base.py b/aws_lambda_powertools/metrics/base.py index 1eece781bbf..a1ffe08caf9 100644 --- a/aws_lambda_powertools/metrics/base.py +++ b/aws_lambda_powertools/metrics/base.py @@ -5,7 +5,7 @@ import os import pathlib from enum import Enum -from typing import Dict, List, Union +from typing import Any, Dict, List, Union import fastjsonschema @@ -78,7 +78,12 @@ class MetricManager: """ def __init__( - self, metric_set: Dict[str, str] = None, dimension_set: Dict = None, namespace: str = None, service: str = None + self, + metric_set: Dict[str, str] = None, + dimension_set: Dict = None, + namespace: str = None, + metadata_set: Dict[str, Any] = None, + service: str = None, ): self.metric_set = metric_set if metric_set is not None else {} self.dimension_set = dimension_set if dimension_set is not None else {} @@ -86,6 +91,7 @@ def __init__( self.service = service or os.environ.get("POWERTOOLS_SERVICE_NAME") self._metric_units = [unit.value for unit in MetricUnit] self._metric_unit_options = list(MetricUnit.__members__) + self.metadata_set = self.metadata_set if metadata_set is not None else {} def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]): """Adds given metric @@ -131,7 +137,7 @@ def add_metric(self, name: str, unit: MetricUnit, value: Union[float, int]): # since we could have more than 100 metrics self.metric_set.clear() - def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None) -> Dict: + def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None, metadata: Dict = None) -> Dict: """Serializes metric and dimensions set Parameters @@ -165,39 +171,48 @@ def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None) -> if dimensions is None: # pragma: no cover dimensions = self.dimension_set + if metadata is None: # pragma: no cover + metadata = self.metadata_set + if self.service and not self.dimension_set.get("service"): self.dimension_set["service"] = self.service logger.debug("Serializing...", {"metrics": metrics, "dimensions": dimensions}) - dimension_keys: List[str] = list(dimensions.keys()) - metric_names_unit: List[Dict[str, str]] = [] - metric_set: Dict[str, str] = {} + metric_names_and_units: List[Dict[str, str]] = [] # [ { "Name": "metric_name", "Unit": "Count" } ] + metric_names_and_values: Dict[str, str] = {} # { "metric_name": 1.0 } for metric_name in metrics: metric: str = metrics[metric_name] metric_value: int = metric.get("Value", 0) metric_unit: str = metric.get("Unit", "") - metric_names_unit.append({"Name": metric_name, "Unit": metric_unit}) - metric_set.update({metric_name: metric_value}) - - metrics_definition = { - "CloudWatchMetrics": [ - {"Namespace": self.namespace, "Dimensions": [dimension_keys], "Metrics": metric_names_unit} - ] + metric_names_and_units.append({"Name": metric_name, "Unit": metric_unit}) + metric_names_and_values.update({metric_name: metric_value}) + + embedded_metrics_object = { + "_aws": { + "Timestamp": int(datetime.datetime.now().timestamp() * 1000), # epoch + "CloudWatchMetrics": [ + { + "Namespace": self.namespace, # "test_namespace" + "Dimensions": [list(dimensions.keys())], # [ "service" ] + "Metrics": metric_names_and_units, + } + ], + }, + **dimensions, # "service": "test_service" + **metadata, # "username": "test" + **metric_names_and_values, # "single_metric": 1.0 } - metrics_timestamp = {"Timestamp": int(datetime.datetime.now().timestamp() * 1000)} - metric_set["_aws"] = {**metrics_timestamp, **metrics_definition} - metric_set.update(**dimensions) try: - logger.debug("Validating serialized metrics against CloudWatch EMF schema", metric_set) - fastjsonschema.validate(definition=CLOUDWATCH_EMF_SCHEMA, data=metric_set) + logger.debug("Validating serialized metrics against CloudWatch EMF schema", embedded_metrics_object) + fastjsonschema.validate(definition=CLOUDWATCH_EMF_SCHEMA, data=embedded_metrics_object) except fastjsonschema.JsonSchemaException as e: message = f"Invalid format. Error: {e.message}, Invalid item: {e.name}" # noqa: B306, E501 raise SchemaValidationError(message) - return metric_set + return embedded_metrics_object def add_dimension(self, name: str, value: str): """Adds given dimension to all metrics @@ -225,6 +240,38 @@ def add_dimension(self, name: str, value: str): else: self.dimension_set[name] = str(value) + def add_metadata(self, key: str, value: Any): + """Adds high cardinal metadata for metrics object + + This will not be available during metrics visualization. + Instead, this will be searchable through logs. + + If you're looking to add metadata to filter metrics, then + use add_dimensions method. + + Example + ------- + **Add metrics metadata** + + metric.add_metadata(key="booking_id", value="booking_id") + + Parameters + ---------- + name : str + Metadata key + value : any + Metadata value + """ + logger.debug(f"Adding metadata: {key}:{value}") + + # Cast key to str according to EMF spec + # Majority of keys are expected to be string already, so + # checking before casting improves performance in most cases + if isinstance(key, str): + self.metadata_set[key] = value + else: + self.metadata_set[str(key)] = value + def __extract_metric_unit_value(self, unit: Union[str, MetricUnit]) -> str: """Return metric value from metric unit whether that's str or MetricUnit enum diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index 43cbeea2dc1..b16f28e92f0 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -67,20 +67,28 @@ def do_something(): _metrics = {} _dimensions = {} + _metadata = {} def __init__(self, service: str = None, namespace: str = None): self.metric_set = self._metrics self.dimension_set = self._dimensions self.service = service self.namespace = namespace + self.metadata_set = self._metadata + super().__init__( - metric_set=self.metric_set, dimension_set=self.dimension_set, namespace=self.namespace, service=self.service + metric_set=self.metric_set, + dimension_set=self.dimension_set, + namespace=self.namespace, + metadata_set=self.metadata_set, + service=self.service, ) def clear_metrics(self): logger.debug("Clearing out existing metric set from memory") self.metric_set.clear() self.dimension_set.clear() + self.metadata_set.clear() def log_metrics( self, diff --git a/docs/content/core/metrics.mdx b/docs/content/core/metrics.mdx index e8767e1b6d4..7265d9b8a50 100644 --- a/docs/content/core/metrics.mdx +++ b/docs/content/core/metrics.mdx @@ -89,6 +89,56 @@ with single_metric(name="ColdStart", unit=MetricUnit.Count, value=1, namespace=" ... ``` +## Adding metadata + +You can use `add_metadata` for advanced use cases, where you want to metadata as part of the serialized metrics object. + + + This will not be available during metrics visualization - Use dimensions for this purpose +
+ +```python:title=app.py +from aws_lambda_powertools import Metrics +from aws_lambda_powertools.metrics import MetricUnit + +metrics = Metrics(namespace="ExampleApplication", service="booking") +metrics.add_metric(name="SuccessfulBooking", unit=MetricUnit.Count, value=1) +metrics.add_metadata(key="booking_id", value="booking_uuid") # highlight-line +``` + +This will be available in CloudWatch Logs to ease operations on high cardinal data. + +
+Exerpt output in CloudWatch Logs + +```json:title=cloudwatch_logs.json +{ + "SuccessfulBooking": 1.0, + "_aws": { + "Timestamp": 1592234975665, + "CloudWatchMetrics": [ + { + "Namespace": "ExampleApplication", + "Dimensions": [ + [ + "service" + ] + ], + "Metrics": [ + { + "Name": "SuccessfulBooking", + "Unit": "Count" + } + ] + } + ] + }, + "service": "booking", + "booking_id": "booking_uuid" // highlight-line +} +``` +
+ ## Flushing metrics As you finish adding all your metrics, you need to serialize and flush them to standard output. You can do that right before you return your response to the caller via `log_metrics`. diff --git a/tests/functional/test_metrics.py b/tests/functional/test_metrics.py index efc93daa739..3407441a7bc 100644 --- a/tests/functional/test_metrics.py +++ b/tests/functional/test_metrics.py @@ -61,12 +61,19 @@ def service() -> str: return "test_service" +@pytest.fixture +def metadata() -> Dict[str, str]: + return {"key": "username", "value": "test"} + + @pytest.fixture def a_hundred_metrics(namespace=namespace) -> List[Dict[str, str]]: return [{"name": f"metric_{i}", "unit": "Count", "value": 1} for i in range(100)] -def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: str) -> Dict: +def serialize_metrics( + metrics: List[Dict], dimensions: List[Dict], namespace: str, metadatas: List[Dict] = None +) -> Dict: """ Helper function to build EMF object from a list of metrics, dimensions """ my_metrics = MetricManager(namespace=namespace) for dimension in dimensions: @@ -75,15 +82,23 @@ def serialize_metrics(metrics: List[Dict], dimensions: List[Dict], namespace: st for metric in metrics: my_metrics.add_metric(**metric) + if metadatas is not None: + for metadata in metadatas: + my_metrics.add_metadata(**metadata) + if len(metrics) != 100: return my_metrics.serialize_metric_set() -def serialize_single_metric(metric: Dict, dimension: Dict, namespace: str) -> Dict: +def serialize_single_metric(metric: Dict, dimension: Dict, namespace: str, metadata: Dict = None) -> Dict: """ Helper function to build EMF object from a given metric, dimension and namespace """ my_metrics = MetricManager(namespace=namespace) my_metrics.add_metric(**metric) my_metrics.add_dimension(**dimension) + + if metadata is not None: + my_metrics.add_metadata(**metadata) + return my_metrics.serialize_metric_set() @@ -533,3 +548,85 @@ def lambda_handler(evt, ctx): for metric_record in second_output["_aws"]["CloudWatchMetrics"]: assert ["service"] in metric_record["Dimensions"] + + +def test_add_metadata_non_string_dimension_keys(service, metric, namespace): + # GIVEN Metrics is initialized + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) + + # WHEN we utilize add_metadata with non-string keys + my_metrics.add_metadata(key=10, value="number_ten") + + # THEN we should have no exceptions + # and dimension values should be serialized as strings + expected_metadata = {"10": "number_ten"} + assert my_metrics.metadata_set == expected_metadata + + +def test_add_metadata(service, metric, namespace, metadata): + # GIVEN Metrics is initialized + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) + + # WHEN we utilize add_metadata with non-string keys + my_metrics.add_metadata(**metadata) + + # THEN we should have no exceptions + # and dimension values should be serialized as strings + assert my_metrics.metadata_set == {metadata["key"]: metadata["value"]} + + +def test_log_metrics_with_metadata(capsys, metric, dimension, namespace, service, metadata): + # GIVEN Metrics is initialized + my_metrics = Metrics(namespace=namespace) + my_metrics.add_metric(**metric) + my_metrics.add_dimension(**dimension) + + # WHEN we utilize log_metrics to serialize and add metadata + @my_metrics.log_metrics + def lambda_handler(evt, ctx): + my_metrics.add_metadata(**metadata) + pass + + lambda_handler({}, {}) + + output = capture_metrics_output(capsys) + expected = serialize_single_metric(metric=metric, dimension=dimension, namespace=namespace, metadata=metadata) + + # THEN we should have no exceptions and metadata + remove_timestamp(metrics=[output, expected]) + assert expected == output + + +def test_serialize_metric_set_metric_definition(metric, dimension, namespace, service, metadata): + expected_metric_definition = { + "single_metric": 1.0, + "_aws": { + "Timestamp": 1592237875494, + "CloudWatchMetrics": [ + { + "Namespace": "test_namespace", + "Dimensions": [["test_dimension", "service"]], + "Metrics": [{"Name": "single_metric", "Unit": "Count"}], + } + ], + }, + "service": "test_service", + "username": "test", + "test_dimension": "test", + } + + # GIVEN Metrics is initialized + my_metrics = Metrics(service=service, namespace=namespace) + my_metrics.add_metric(**metric) + my_metrics.add_dimension(**dimension) + my_metrics.add_metadata(**metadata) + + # WHEN metrics are serialized manually + metric_definition_output = my_metrics.serialize_metric_set() + + # THEN we should emit a valid embedded metric definition object + assert "Timestamp" in metric_definition_output["_aws"] + remove_timestamp(metrics=[metric_definition_output, expected_metric_definition]) + assert metric_definition_output == expected_metric_definition From 05ed3fe5bc2475266d751c40adcaa95181f3eaa5 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Wed, 17 Jun 2020 17:57:45 +0100 Subject: [PATCH 6/8] feat: docs anonymized page view (#82) * chore: add amplify dep Signed-off-by: heitorlessa * feat: add anonymized click stream to docs view Signed-off-by: heitorlessa --- docs/gatsby-browser.js | 25 + docs/package-lock.json | 1573 ++++++++++++++++++++++++++++++++++++++++ docs/package.json | 2 + docs/src/config.js | 12 + 4 files changed, 1612 insertions(+) create mode 100644 docs/src/config.js diff --git a/docs/gatsby-browser.js b/docs/gatsby-browser.js index 8b843acc9bc..d029790c080 100644 --- a/docs/gatsby-browser.js +++ b/docs/gatsby-browser.js @@ -1 +1,26 @@ import "./src/styles/global.css" +import Amplify from 'aws-amplify'; +import { Analytics, AWSKinesisFirehoseProvider } from '@aws-amplify/analytics'; +import awsconfig from './src/config'; + +export const onRouteUpdate = ({ location, prevLocation }) => { + Analytics.record({ + data: { + url: window.location.href, + section: location.pathname, + previous: prevLocation ? prevLocation.pathname : null + }, + streamName: awsconfig.aws_kinesis_firehose_stream_name + }, 'AWSKinesisFirehose') +} + +export const onClientEntry = () => { + Analytics.addPluggable(new AWSKinesisFirehoseProvider()); + Amplify.configure(awsconfig); + + Analytics.configure({ + AWSKinesisFirehose: { + region: awsconfig.aws_project_region + } + }); +} diff --git a/docs/package-lock.json b/docs/package-lock.json index eb87975e8e0..5d234638fd4 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -57,6 +57,1368 @@ "tinycolor2": "^1.4.1" } }, + "@aws-amplify/analytics": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@aws-amplify/analytics/-/analytics-3.2.0.tgz", + "integrity": "sha512-p0ZlT6LVMNV44m+4fQlgr3u2XUmk19Us5XoVoWA5SDiZGrs0ae9yxeq8QiPZCx6+QYWZIZ8eXn4wGxqdNUz1xw==", + "requires": { + "@aws-amplify/cache": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "@aws-sdk/client-firehose": "1.0.0-gamma.2", + "@aws-sdk/client-kinesis": "1.0.0-gamma.2", + "@aws-sdk/client-personalize-events": "1.0.0-gamma.2", + "@aws-sdk/client-pinpoint": "1.0.0-gamma.2", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "uuid": "^3.2.1" + } + }, + "@aws-amplify/api": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/@aws-amplify/api/-/api-3.1.16.tgz", + "integrity": "sha512-sGHaEiq5OmfCiSHlayx5FrWdgXKLVDLXUGKyUNdX/r7Z7JDaVA5gd5DsQ9pJiDTWzslIAfWTiZNUfOFHxfumxg==", + "requires": { + "@aws-amplify/api-graphql": "^1.0.18", + "@aws-amplify/api-rest": "^1.0.18" + } + }, + "@aws-amplify/api-graphql": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@aws-amplify/api-graphql/-/api-graphql-1.0.18.tgz", + "integrity": "sha512-H7K/7vmszM6ldiedRo9mBG2CCoVnuIMnu84i6Y+szAbEezDxjVJae0SdVlXLHtDRAZcGCmgS7Lly7fsYvXTIKg==", + "requires": { + "@aws-amplify/api-rest": "^1.0.18", + "@aws-amplify/auth": "^3.2.13", + "@aws-amplify/cache": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "@aws-amplify/pubsub": "^3.0.17", + "graphql": "14.0.0", + "uuid": "^3.2.1", + "zen-observable-ts": "0.8.19" + }, + "dependencies": { + "graphql": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.0.0.tgz", + "integrity": "sha512-HGVcnO6B25YZcSt6ZsH6/N+XkYuPA7yMqJmlJ4JWxWlS4Tr8SHI56R1Ocs8Eor7V7joEZPRXPDH8RRdll1w44Q==", + "requires": { + "iterall": "^1.2.2" + } + } + } + }, + "@aws-amplify/api-rest": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/@aws-amplify/api-rest/-/api-rest-1.0.18.tgz", + "integrity": "sha512-yCTMbfa858GF+yboh32enQsxavKDz4ExF963oShb++E9Al1pxPCAiXVuf7ZfIQIZUpM61koe9vnN7DjS8t9dnA==", + "requires": { + "@aws-amplify/core": "^3.3.3", + "axios": "0.19.0" + }, + "dependencies": { + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + } + } + }, + "@aws-amplify/auth": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@aws-amplify/auth/-/auth-3.2.13.tgz", + "integrity": "sha512-DgXCsJvQewxfz7VkSv6cfxupytrbPT2XPhs6W9FAf0CIhy+v6Ow9EIlnKsNWDI7yFrLX0AzXtwWdK0U/3X4dzg==", + "requires": { + "@aws-amplify/cache": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "amazon-cognito-identity-js": "^4.3.1", + "crypto-js": "^3.3.0" + } + }, + "@aws-amplify/cache": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/@aws-amplify/cache/-/cache-3.1.16.tgz", + "integrity": "sha512-CAJzuHIUyPKv6Ns8zRuiqjeK87Zn6OaE60580vMqDXplmUHwIxNlsrWrQZobLalZWYAX++QghnXT0B842JYvgg==", + "requires": { + "@aws-amplify/core": "^3.3.3" + } + }, + "@aws-amplify/core": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@aws-amplify/core/-/core-3.3.3.tgz", + "integrity": "sha512-fIrvaP8Sq4JovriVAK863BOWQsYiZEDgnXKul+W9Z46Xp9z0FjDWtI3oGuo5auQju64EVfibd7OUOeaujS4zTQ==", + "requires": { + "@aws-crypto/sha256-js": "1.0.0-alpha.0", + "@aws-sdk/client-cognito-identity": "1.0.0-gamma.2", + "@aws-sdk/credential-provider-cognito-identity": "1.0.0-gamma.2", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-hex-encoding": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "url": "^0.11.0", + "zen-observable-ts": "0.8.19" + } + }, + "@aws-amplify/datastore": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@aws-amplify/datastore/-/datastore-2.2.3.tgz", + "integrity": "sha512-Pg3WkQgSpDL1WXmRIax+dbuGtz5vpzr0kkbCnVLMEcfW5YOFkENj37/z0rY4zVZKgVLCDYDdgho2DxCwXbnpjQ==", + "requires": { + "@aws-amplify/api": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "@aws-amplify/pubsub": "^3.0.17", + "idb": "5.0.2", + "immer": "6.0.1", + "ulid": "2.3.0", + "uuid": "3.3.2", + "zen-observable-ts": "0.8.19", + "zen-push": "0.2.1" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, + "@aws-amplify/interactions": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/@aws-amplify/interactions/-/interactions-3.1.16.tgz", + "integrity": "sha512-6s6WuDDEZ5zJ6gK2UH1LF1Nk6wzkUNjERNnJbucGjXlsI3oCtz2eyPMjTTRTE9a2MJRjxD35zcJKfMb7GKHzwg==", + "requires": { + "@aws-amplify/core": "^3.3.3", + "@aws-sdk/client-lex-runtime-service": "1.0.0-gamma.2" + } + }, + "@aws-amplify/predictions": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/@aws-amplify/predictions/-/predictions-3.1.16.tgz", + "integrity": "sha512-DrAeOfgoYdD5AYwNQ2Q6kUxQbMUn0vI5rJa0or/Sall3geB1Q25TQJUiqkhk5n5zjpc7rIs6+RtnvR22XCyDnA==", + "requires": { + "@aws-amplify/core": "^3.3.3", + "@aws-amplify/storage": "^3.2.6", + "@aws-sdk/client-comprehend": "1.0.0-gamma.2", + "@aws-sdk/client-polly": "1.0.0-gamma.2", + "@aws-sdk/client-rekognition": "1.0.0-gamma.2", + "@aws-sdk/client-textract": "1.0.0-gamma.2", + "@aws-sdk/client-translate": "1.0.0-gamma.2", + "@aws-sdk/eventstream-marshaller": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "uuid": "^3.2.1" + } + }, + "@aws-amplify/pubsub": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@aws-amplify/pubsub/-/pubsub-3.0.17.tgz", + "integrity": "sha512-Okml+NnMxdCiZT/TMzgn1IOpK4Wi4Ve1rOWILeZLK/LJHWYekquoH3t5wuFE1Gx/ujSVilPhBjasPJwvSCbwnw==", + "requires": { + "@aws-amplify/auth": "^3.2.13", + "@aws-amplify/cache": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "graphql": "14.0.0", + "paho-mqtt": "^1.1.0", + "uuid": "^3.2.1", + "zen-observable-ts": "0.8.19" + }, + "dependencies": { + "graphql": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.0.0.tgz", + "integrity": "sha512-HGVcnO6B25YZcSt6ZsH6/N+XkYuPA7yMqJmlJ4JWxWlS4Tr8SHI56R1Ocs8Eor7V7joEZPRXPDH8RRdll1w44Q==", + "requires": { + "iterall": "^1.2.2" + } + } + } + }, + "@aws-amplify/storage": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@aws-amplify/storage/-/storage-3.2.6.tgz", + "integrity": "sha512-FMdXR78wuRJ3n7Cc6aywJZMdcdy5fmbxawm6zSOZGefFrH+x6aY5xG1fIKAoVSTESS3SPGI2YyIhyl+8+7xIIw==", + "requires": { + "@aws-amplify/core": "^3.3.3", + "@aws-sdk/client-s3": "1.0.0-gamma.2", + "@aws-sdk/s3-request-presigner": "1.0.0-gamma.1", + "@aws-sdk/util-create-request": "1.0.0-gamma.1", + "@aws-sdk/util-format-url": "1.0.0-gamma.1", + "axios": "0.19.0", + "events": "^3.1.0", + "sinon": "^7.5.0" + }, + "dependencies": { + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + } + } + }, + "@aws-amplify/ui": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@aws-amplify/ui/-/ui-2.0.2.tgz", + "integrity": "sha512-OLdZmUCVK29+JV8PrkgVPjg+GIFtBnNjhC0JSRgrps+ynOFkibMQQPKeFXlTYtlukuCuepCelPSkjxvhcLq2ZA==" + }, + "@aws-amplify/xr": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@aws-amplify/xr/-/xr-2.1.16.tgz", + "integrity": "sha512-mexbmwkHJZ9Y0ISIw9S69my+JWtzAzcnnWdx0qlhtHT/nMySevYWoK6dCEQV24eoypGt9AkNe65A4RwKCvrYJQ==", + "requires": { + "@aws-amplify/core": "^3.3.3" + } + }, + "@aws-crypto/crc32": { + "version": "1.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-1.0.0-alpha.0.tgz", + "integrity": "sha512-n4OJttn49liBR0CVdK7dAvkTaP8jLiRRekdA0wunTEELIIwjC4c60YODADbqR2Hug4dtzQ6huJTgyFeHIaYPHg==", + "requires": { + "tslib": "^1.9.3" + } + }, + "@aws-crypto/ie11-detection": { + "version": "1.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0-alpha.0.tgz", + "integrity": "sha512-TQ55S96+aD/iZF/VdgbLqCm2um8mQhjNrlFqQEJkXc12L4taF0wz0FfdFSJ9Uuy6EIf4GjgvbLExgJwxmFqL5A==", + "requires": { + "tslib": "^1.9.3" + } + }, + "@aws-crypto/sha256-browser": { + "version": "1.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-1.0.0-alpha.0.tgz", + "integrity": "sha512-ZhULGaJKI/o8KROknqvnmYX3gphPQL5HLoMdVD5yPEsEsFG7rEIu4ORv2s6uaiqkdEkXZcdS+CNC8ekIndr9QA==", + "requires": { + "@aws-crypto/ie11-detection": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-crypto/supports-web-crypto": "^1.0.0-alpha.0", + "@aws-sdk/types": "^1.0.0-alpha.0", + "@aws-sdk/util-locate-window": "^1.0.0-alpha.0", + "@aws-sdk/util-utf8-browser": "^1.0.0-alpha.0", + "tslib": "^1.9.3" + } + }, + "@aws-crypto/sha256-js": { + "version": "1.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.0.0-alpha.0.tgz", + "integrity": "sha512-GidX2lccEtHZw8mXDKJQj6tea7qh3pAnsNSp1eZNxsN4MMu2OvSraPSqiB1EihsQkZBMg0IiZPpZHoACUX/QMQ==", + "requires": { + "@aws-sdk/types": "^1.0.0-alpha.0", + "@aws-sdk/util-utf8-browser": "^1.0.0-alpha.0", + "tslib": "^1.9.3" + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "1.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0-alpha.0.tgz", + "integrity": "sha512-jVWjNCoEKY49NIWyU1ia1RvtupEZEzOTkYZ1kRH+Z0RqIg9DZksQ7PbSRvxtAv8rTBdyGSgQdEpbFtQtm/ZiRQ==", + "requires": { + "tslib": "^1.9.3" + } + }, + "@aws-sdk/abort-controller": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-1.0.0-gamma.1.tgz", + "integrity": "sha512-ShIcthHm+mTUgif9cwJDIrOIG/A30HSoA9WdXSCE8lrQ83D0AUTtBMAWwlN4ZuTf9ABzIwBQ/w9wZZpla650eA==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/chunked-blob-reader": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-1.0.0-gamma.1.tgz", + "integrity": "sha512-MZNwCD+A8x9jQsj7Wn3sRFZaj2evWQjVL1hv2gRcr7cc8lG7gIo6TN/IFyVTB5V0eMNoJu8Ej9MXMo98EO0THA==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/chunked-blob-reader-native": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-1.0.0-gamma.1.tgz", + "integrity": "sha512-muQUjB6RBjWq94HHBWWMdwIxfwwlZyKb2zTIH7R6nHZZI0IUhrhQc1PJC0dveD+1DTJ3fhTl9n2WrCJHT0uXnQ==", + "requires": { + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-cognito-identity": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-1.0.0-gamma.2.tgz", + "integrity": "sha512-gGwTrKRN+mj48m4tXTilTbp1/aYO5mGIBPocDsG7digNFqxmjzFVNCq/yJfoBg7MCNBVlOFJ7i3bxM6H4cylow==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-comprehend": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-comprehend/-/client-comprehend-1.0.0-gamma.2.tgz", + "integrity": "sha512-WOkDZI/kvA0DYR5N9zsYXkvQu749vSQfA3W7Ddj4Ln6+jCgB3fA8kEejLisNzPjKI9OWVdbdqkYXvejtynykTg==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0", + "uuid": "^7.0.0" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "@aws-sdk/client-firehose": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-1.0.0-gamma.2.tgz", + "integrity": "sha512-D972+fTX1mfLbMu99bEEsrh27eLWx2YnGyHb5jDCTYQLdHT63OfWVxikj/NTtO2LgO7yfPxAe35UGHxthqdJ4A==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-kinesis": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-1.0.0-gamma.2.tgz", + "integrity": "sha512-ysuQ+m9waw2CdbX64h1ap1aPxip+12ck3lISYQ/iPqbhPVH6gkK3MxM2bs95Ploh6Nzrc+g9pUuAAyvAZ66oUg==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-browser": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-config-resolver": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-lex-runtime-service": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lex-runtime-service/-/client-lex-runtime-service-1.0.0-gamma.2.tgz", + "integrity": "sha512-rVjfPWFTqUk0dJQjebLjKzcb+uFrfP+AqWz2OE/1HLjwDA5Zu/P0kC30Ch/S/IiKYo9TpGFMDd1KNk3umVLrpw==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-personalize-events": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-personalize-events/-/client-personalize-events-1.0.0-gamma.2.tgz", + "integrity": "sha512-LWt6KkA2CqbI9Bai4mRAni+QVQTyk04FREMrkwrs4fq+Zk2TQnvcu938eSFiUjsoT8l5gyDxYaPflhA6eyNx5Q==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-pinpoint": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-pinpoint/-/client-pinpoint-1.0.0-gamma.2.tgz", + "integrity": "sha512-2J7MOjwZs+9Eugo4i4Se0Kha9WE59Toq+Zl4V1oNCFaridOSsOcAlItTHYcTxgHUOWF+d2bJ8NQ2UiGlEcJvsg==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-polly": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-polly/-/client-polly-1.0.0-gamma.2.tgz", + "integrity": "sha512-qeKEngvEuX6kAgyvw1WavI8V8K25+CPa6ZQiDNsAww256QWZKSCacujTWWvS5/fqib36UaPDwLod+2iFxxBhnA==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-rekognition": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-rekognition/-/client-rekognition-1.0.0-gamma.2.tgz", + "integrity": "sha512-Solfdy09kKS7iV1CfBNg853M5rttoS13FJ0Ik1Fczx/OJj7Z8ob8EdxJ2XuhB8/zsQlG1SiXQQ54bw8o+Kc9oA==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-s3": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-1.0.0-gamma.2.tgz", + "integrity": "sha512-v5V+/S6X1iyb9JfEFmHMHstt2lJm7PArLm9ursYlv7QUWX4aN7jAgzRFNIxs6YvG+vrlVWg5B3lFKVeN+vF7GA==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-browser": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-config-resolver": "1.0.0-gamma.1", + "@aws-sdk/eventstream-serde-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-blob-browser": "1.0.0-gamma.1", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/hash-stream-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/md5-js": "1.0.0-gamma.1", + "@aws-sdk/middleware-apply-body-checksum": "1.0.0-gamma.1", + "@aws-sdk/middleware-bucket-endpoint": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-expect-continue": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-location-constraint": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-sdk-s3": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-ssec": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "@aws-sdk/xml-builder": "1.0.0-gamma.1", + "fast-xml-parser": "^3.16.0", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-textract": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-textract/-/client-textract-1.0.0-gamma.2.tgz", + "integrity": "sha512-omK+qwUJ9zibFU3Hh7kLqUIZxUmYu6LKHQVJPgZMnHmQEV5meccEioHbDUqoKMY2PapbGHWIN0S6kgCk7Ij5JA==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/client-translate": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-translate/-/client-translate-1.0.0-gamma.2.tgz", + "integrity": "sha512-AHYw6Mlpo1Rf+EGn7tcgCDBeQpvTnyAeh6t0B/dP0WbgevVzCR5wvG+PQvmoNiR+kgKKhfi+ah2CFF5xXZBA6g==", + "requires": { + "@aws-crypto/sha256-browser": "^1.0.0-alpha.0", + "@aws-crypto/sha256-js": "^1.0.0-alpha.0", + "@aws-sdk/config-resolver": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-node": "1.0.0-gamma.1", + "@aws-sdk/fetch-http-handler": "1.0.0-gamma.2", + "@aws-sdk/hash-node": "1.0.0-gamma.1", + "@aws-sdk/invalid-dependency": "1.0.0-gamma.1", + "@aws-sdk/middleware-content-length": "1.0.0-gamma.1", + "@aws-sdk/middleware-host-header": "1.0.0-gamma.1", + "@aws-sdk/middleware-retry": "1.0.0-gamma.1", + "@aws-sdk/middleware-serde": "1.0.0-gamma.1", + "@aws-sdk/middleware-signing": "1.0.0-gamma.1", + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/middleware-user-agent": "1.0.0-gamma.1", + "@aws-sdk/node-http-handler": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/region-provider": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/url-parser-browser": "1.0.0-gamma.1", + "@aws-sdk/url-parser-node": "1.0.0-gamma.1", + "@aws-sdk/util-base64-browser": "1.0.0-gamma.1", + "@aws-sdk/util-base64-node": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-browser": "1.0.0-gamma.1", + "@aws-sdk/util-body-length-node": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-browser": "1.0.0-gamma.1", + "@aws-sdk/util-user-agent-node": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-node": "1.0.0-gamma.1", + "tslib": "^1.8.0", + "uuid": "^7.0.0" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "@aws-sdk/config-resolver": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-1.0.0-gamma.1.tgz", + "integrity": "sha512-pBmOberuJ35eZ1Svqsu8B8vvHv8z6ilmnmhQ4wuy+QhyR22f4rzD/23wnNyAgK/OKvTPzwxaf0DIMF2x5p5yrA==", + "requires": { + "@aws-sdk/signature-v4": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-cognito-identity": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-1.0.0-gamma.2.tgz", + "integrity": "sha512-mQSWij2FeyTnJqSwVOVxB6EqIxP0JfSk31wplRMwIFs1JEe2s4CbR6WkgfJdwBfK+uTbZGyR24EtUtlQiSP5zw==", + "requires": { + "@aws-sdk/client-cognito-identity": "1.0.0-gamma.2", + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-1.0.0-gamma.1.tgz", + "integrity": "sha512-RB3aZNHNsPojQFzEbds7vPVus/HY+p6EqAVlH7mX8L7ACYBd6Gxtnxs+BKFMQA38Ev86oDbBoW93f1ppjjDHIg==", + "requires": { + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-imds": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-1.0.0-gamma.1.tgz", + "integrity": "sha512-uf1wnGkOf3recCcaFY8OyFqaCZs4I27ETooxUg2j/PiAVuLDGhe+AjAU4jtZqthg8ECDTT73LnfGb3S6tuc3Eg==", + "requires": { + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-1.0.0-gamma.1.tgz", + "integrity": "sha512-naocfWbP6l3lWbxmfuSarphadPs87cRVwYpZ9FhQwzXb7Ff7rsWsZVFDBMqn/K0CH9rGZeKEcR1HzfKWx2zQ1w==", + "requires": { + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/shared-ini-file-loader": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-DBFPoZlmWt3PoQ+pOG4rJRBXw8ofhUaZrKD5Nm7/6Qs0JSbzWPlXSGDDUw1PdyThstFC1GVN/tAF2RhePa4mng==", + "requires": { + "@aws-sdk/credential-provider-env": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-imds": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-ini": "1.0.0-gamma.1", + "@aws-sdk/credential-provider-process": "1.0.0-gamma.1", + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-1.0.0-gamma.1.tgz", + "integrity": "sha512-gxVOXRF/XR+tfa516tvsy54xqXSvbJuW5gIpVWnUqPfZAesnz5Yh7/AIlWm1e7UHKGG1pDTPRmEKhrXwfyna/Q==", + "requires": { + "@aws-sdk/credential-provider-ini": "1.0.0-gamma.1", + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/shared-ini-file-loader": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/eventstream-marshaller": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-1.0.0-gamma.1.tgz", + "integrity": "sha512-qEJdol/mMGfiGFuHGtDKyiqynpWH+819Ja8RZOctw1Qxi6OeHKXTto5M5frJDw+bVgnYlxRKWng96hg9SeMuiA==", + "requires": { + "@aws-crypto/crc32": "^1.0.0-alpha.0", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-hex-encoding": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/eventstream-serde-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-n9460HbQzS3hVfFUUQbwzLu6t4j5rQEzoNLJWNuYZdkMLkue40Wv1Da2aUcM8kTsuUwSxMZYe7aHfUWaSktrPg==", + "requires": { + "@aws-sdk/eventstream-marshaller": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/eventstream-serde-config-resolver": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-1.0.0-gamma.1.tgz", + "integrity": "sha512-ZUnZGwbYX+6XBseOnKPiG+WcdIf4PS5o/K9uFuMHeY+dfeEYH/uiP1VFaQDugMnMeXAWF9sOzTt1vcYWhbEDCw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/eventstream-serde-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-DSN4p4k2+nnx4XDMWKb9a4sggXttkBxQTVqU6HegLR7oM2OeVDoXgI3g09qhdpV9Bbw27STXk6BTu8dzNL2ZxA==", + "requires": { + "@aws-sdk/eventstream-marshaller": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/fetch-http-handler": { + "version": "1.0.0-gamma.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-1.0.0-gamma.2.tgz", + "integrity": "sha512-uSMmGElKKeClA7yVPipZLTPMGXLz1WiQB4utTEAxrgfOFDHIjSkTyAcPELdcB/VU+DmvMeSmPn9IOTQKqwv80g==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/querystring-builder": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/hash-blob-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-YosiC6jsq7gKkNAoQn9cOMpV+r7pqvQBS1pU/8bDmaeDFL/BVeettNINKHh9BjZGvdArXJArH8958+3zTb6+ug==", + "requires": { + "@aws-sdk/chunked-blob-reader": "1.0.0-gamma.1", + "@aws-sdk/chunked-blob-reader-native": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/hash-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-NH2aPVm857rWhuL3eRHllE3qHVGevEtLWxOy+dz7i/2gsTJ+nbkc9YihhNxbdAdwO6qEBM4AcnWHS0G1w1F9rA==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-buffer-from": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/hash-stream-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-dW3vP0KbFQ14wfIg1JI+xxBoGOovipbtepFiolhT9JV1b+NnllfCK79oO6BMeRYbrnCWw1xrlMFGTDlXtio0vw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/invalid-dependency": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-1.0.0-gamma.1.tgz", + "integrity": "sha512-XAWP6e1nITXGdq9rfvYiId3z6wN0uqBrxwnf0PapElDQeAyQRfpOSjhSfsq2S6PjaTQhbtDsu0QhYpWP1IXhsQ==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/is-array-buffer": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-1.0.0-gamma.1.tgz", + "integrity": "sha512-Oj9mpM19H/3mPDECIHS2K4sZYyfMPBsL+8VkCnwU2/+AJABoxgbf5VjXdXmWZUBh7+8Roa0th2aGXb0WG/QUBg==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/md5-js": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-1.0.0-gamma.1.tgz", + "integrity": "sha512-tvmBETS6M8q8z2Dbw6eHoY2YcToxaJi4/uf9l7bj99275TIwOAt+WQwNLqj3zvfYV3+5TzhfaumnPWnozFNwUg==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-utf8-browser": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-apply-body-checksum": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-1.0.0-gamma.1.tgz", + "integrity": "sha512-0pgmuOAdIKEVmWAtLhcOvdHOTjzcALm8lwf//EnElObu8GfWiFzgjLN3+zBcgw8YST6PZap7hWz3EVCy9RNOBw==", + "requires": { + "@aws-sdk/is-array-buffer": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-bucket-endpoint": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-1.0.0-gamma.1.tgz", + "integrity": "sha512-YZRYMa7NSjxNbdK61PLDKRR2Ox1BnwWLv4pJBdQZuBC9/PYNCvspgDlIBnain8R3t5lKs5JjFG3SG9yttQxSqQ==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-content-length": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-1.0.0-gamma.1.tgz", + "integrity": "sha512-i41usSfqgcgE7d36svQ5wkk/3sKfhr1Gfd9tedFh7g91dChd9v5Xzgm7vk/p1HGUP2kNUPlgXD3xATVeptAhiA==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-expect-continue": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-1.0.0-gamma.1.tgz", + "integrity": "sha512-xIzjLk8DESMyeTVT2ZcQ/ddlJZiLbcEx+mz03adSaoX7yV6zvqUamH08nnsM/n73UwERXeuiiqPP08SWYpEVwg==", + "requires": { + "@aws-sdk/middleware-header-default": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-header-default": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-header-default/-/middleware-header-default-1.0.0-gamma.1.tgz", + "integrity": "sha512-MEtgH0VPNkOjaSJwTtBh1XgUd6DOutgh5Lbp+gcoYmA+GWxYQttNPHqi/so0qVxDIxRN3vt9gJoDLrH38NDr6w==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-1.0.0-gamma.1.tgz", + "integrity": "sha512-CsWobqCyDC0hAESuoD/UI2PESBvdF+oagxC15oWcA5IrpaA5sEwJoUi9BuuK2FXRR+HDnsu1sHRMUwcAciwmnA==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-location-constraint": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-1.0.0-gamma.1.tgz", + "integrity": "sha512-Kkrvd20OHnVQJ4KZC0kbZLZUFS3vcHj1pTX9Ml5ZU86aSWNbCX/KAidTNFvxwBm8o4jZtGlHVvXHyiclBroCOA==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-retry": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-1.0.0-gamma.1.tgz", + "integrity": "sha512-qVefdYFpnlNLVYTHxZ1FyKG10dsZxysUY4prNRFsSU8UrsqnQ72KZrSFxOmP50qPz1sq4FB9zAb5vi+zm2KtAw==", + "requires": { + "@aws-sdk/service-error-classification": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-sdk-s3": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-1.0.0-gamma.1.tgz", + "integrity": "sha512-nJpJVdnaiwxUgzQFeqmBosWtRvTpnHiUOH/Kg18jdTc9vJ+skZHrw/5rwLwmtpiZwkMfoiRNa7oevXGELxjLTw==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-serde": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-1.0.0-gamma.1.tgz", + "integrity": "sha512-4rhB/x8zGmiN1k12fPG1LOxvK41XE8yOlayQyach2Lct30i38Oel/7wpnYaIXuTGFaY+npC127fgAY3XuMcdcw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-signing": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-1.0.0-gamma.1.tgz", + "integrity": "sha512-JYOBU+WWHoQEjPu+2i71j25jPVuAhkW8fhIIo92WTiqj6txyx4s10eRJATkGCJfwXVazdZ8wrFz3afse9dWUFw==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/signature-v4": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-ssec": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-1.0.0-gamma.1.tgz", + "integrity": "sha512-PmzEaqTuH3Jo+VZa+TgwG/5wGMrpIjwRfze8o0saFzxJxIDqy8Uqwoq7i9DEx5ERYttVa3zyzVSOgfe8wdHPYA==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-stack": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-1.0.0-gamma.1.tgz", + "integrity": "sha512-zQEryY3AGVmNUXNl9N0TV7Uvd66QHyf65DAjHR87gOQfqBal1khjyZ33d7C2MlJm4jrkP6gojsdMxL7/C+uUfw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-1.0.0-gamma.1.tgz", + "integrity": "sha512-+KsKxTwlXYMFBtu5KeanNr6FMdY3qikVQHRALer0GAQjoOwKI7XW8ZLYmKox9JcZ/jYvFLiNtDFf5rwvmSkosA==", + "requires": { + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/node-http-handler": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-1.0.0-gamma.1.tgz", + "integrity": "sha512-f8mttfMzY3MY63daZEVol1K3WEzDD1PGNPLdiZVsgq1f3GcAqnP0GcEK10USOL9l3yI11zL0WyD29Ci08quytQ==", + "requires": { + "@aws-sdk/abort-controller": "1.0.0-gamma.1", + "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/querystring-builder": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/property-provider": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-1.0.0-gamma.1.tgz", + "integrity": "sha512-38BRXvFuegHZxMFBNWmSGzQFJhITyoBoPtsGGueW505qpEtofgBMv3UAHugbas+9MPRvU310aX5QDwth9oIK3w==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/protocol-http": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-1.0.0-gamma.1.tgz", + "integrity": "sha512-+zrlpgAy7A4QCe5rEs8NMFLNMQGEpCIXEZMGgLP4wxS5bo+46aHremuHz8uNd2K172bETZk8OLy5Xyna2dKRcw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/querystring-builder": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-1.0.0-gamma.1.tgz", + "integrity": "sha512-9WOdsGXSCat3T7xnKRpnNNP+jPmLsTFx2HXsIh2eH6n4GIeIW46JaqPM3sHTTLjjOyd2PLzOoY439ax4BqP7Ag==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-uri-escape": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/querystring-parser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-1.0.0-gamma.1.tgz", + "integrity": "sha512-g+ndMrYVG15GgeIE0+uBuhSo7xLOqzr343xcaj1262JCrfwMGMl1r/rEWKZix2GycaBXE3TcNKXgcjXSbuLu+A==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/region-provider": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-provider/-/region-provider-1.0.0-gamma.1.tgz", + "integrity": "sha512-r2HQdBlS6BKglMi6Gg4fnGNbIaqEyMIZwtT2u1RwiMPbKC3VWru1OLzwf2MlsL+JmLIVF/y80iNvVEwArryDlQ==", + "requires": { + "@aws-sdk/property-provider": "1.0.0-gamma.1", + "@aws-sdk/shared-ini-file-loader": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/s3-request-presigner": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-1.0.0-gamma.1.tgz", + "integrity": "sha512-PRTNnv/UHGgx+U5dTbq3L/RqZlHwKQv2lU3ZNezNwLLJKkZqPxvtTo4/xBpZzotwqoRd299vRdkN0IH6bE4sXw==", + "requires": { + "@aws-sdk/signature-v4": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-create-request": "1.0.0-gamma.1", + "@aws-sdk/util-format-url": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/service-error-classification": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-1.0.0-gamma.1.tgz", + "integrity": "sha512-8GiWdH+k3VZHKmW9CjXYFR0lmWXfHJNzd2pAKD//WhuDJjz2GcD7YGn/2OrvOB+p2LAGvbuQA8zmmuhvBoqLWw==" + }, + "@aws-sdk/shared-ini-file-loader": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-1.0.0-gamma.1.tgz", + "integrity": "sha512-oj2Xn0mY2mGsJyweN+O1VNqQguaQIYKjKIWlOKKxX+j5euhRpvx4iNQhqCigE9VXLK/NOw2J/F5JchZrd6YwPw==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/signature-v4": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-1.0.0-gamma.1.tgz", + "integrity": "sha512-PqsAVg+7hBgxZbJfOzrN2eCvPiAtudiekAb8f/lrUrjD5VCt7ybQZ6stA8eoeOMk/aziOHPV/VK588xZxZMciA==", + "requires": { + "@aws-sdk/is-array-buffer": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "@aws-sdk/util-hex-encoding": "1.0.0-gamma.1", + "@aws-sdk/util-uri-escape": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/smithy-client": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-1.0.0-gamma.1.tgz", + "integrity": "sha512-nuRZkwTIXZj7jsJ1RO0PoLzmyUda7SlfjDLPYrLDBtKLO3JSL44wLNOF2N1T4myK4phKlECEez1aEir/vb4QOA==", + "requires": { + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/types": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-1.0.0-gamma.1.tgz", + "integrity": "sha512-K41IMXfj4lCNVjvWarJR7TNaP0sOh6hmbV3fDw9zReZ0t6ehQ4CY9JO2XQEWKnR6njyggmpbi/xNM924HYsgTg==" + }, + "@aws-sdk/url-parser-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser-browser/-/url-parser-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-V5/7WWqmkIzN2OD8BQw6VCO7Mr99OoeN0n8kYnFJegF3rtOW/4MPQISWoVm4mK+hdIAAn04gEesqCon1HkTKLw==", + "requires": { + "@aws-sdk/querystring-parser": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/url-parser-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser-node/-/url-parser-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-791C2njiPVKaqvR+EFMSg5kpihQbZzs0ESDqQrBW2x+I66gCAjrrzznuvDDV46UJb5V9av9f0g7ccZkDn/q4cQ==", + "requires": { + "@aws-sdk/querystring-parser": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0", + "url": "^0.11.0" + } + }, + "@aws-sdk/util-base64-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-5OlqkNnKXrsLo0WGAuGea9clqArZk5qn9KEM2Yu4/gSZ2WNl4lI10m/ig+Zsi26fgdygxtvJg8MZLiK4GyKfqw==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-base64-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-9sel4cZHUoULXmyBSsDlJAu9/kk4d3QB9lnhWnIwkj2iQ0pA9Lg5RNxvOAPkrgFHM3KqXtp2gC/wtPvcSoA6iA==", + "requires": { + "@aws-sdk/util-buffer-from": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-body-length-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-5Sa/+2t1giiHkFfMCmN8bQ9HkKDbwh04yf2kBS/VzeSwjQUMv/GXYF+PHesuaaccDXg7SBaZtXmdeoXpRnK4cQ==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-body-length-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-9or+zbpzi1G78XMXWfvc9cs0aDG6PJywN5Vl41QO9g1AHJ5k6C4XjLQ3FDEXWphnQCFNCyfEJ+4qi/HNjyR1Rg==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-buffer-from": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-1.0.0-gamma.1.tgz", + "integrity": "sha512-Aae/ots79VI0x3HqioK+Podvh/HOAAKC3zHeDvLc1t3WOEwWlWbCalSp9Yi9bXOK2WZgYvHHaAbnCMdbYmxemQ==", + "requires": { + "@aws-sdk/is-array-buffer": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-create-request": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-create-request/-/util-create-request-1.0.0-gamma.1.tgz", + "integrity": "sha512-mSdyKioGyda+28ZweNYi0twubvdJjTaTxptqZ2SU8jf0SeDSRqKoopzVsAZCgjLbYARd9vtRGL0+7i8Ej6O1RA==", + "requires": { + "@aws-sdk/middleware-stack": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-format-url": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-1.0.0-gamma.1.tgz", + "integrity": "sha512-7ysFU8BHqptwK0U3kLP6tpxjMjR4a0dVnm/3lXE//kRMeyZYc8c883UVkhx3rqrvGJmeusJppVSvFURMuHMtjg==", + "requires": { + "@aws-sdk/querystring-builder": "1.0.0-gamma.1", + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-hex-encoding": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-1.0.0-gamma.1.tgz", + "integrity": "sha512-SGOaAgjR3iaPB4obob/gqXPjgmxEN6X3zxWrfFk4jG+pdheKBAniw7ckITdBEG04Gkqh91stIORLIgxR8gxjIg==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-locate-window": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-1.0.0-gamma.1.tgz", + "integrity": "sha512-gKLTmSnpZfdezYMBgEjUFH1jE4VvbxWswQhI4XUs3ChOWdm46AdRBVbGqnN6hlo9dSZwJQaeRE9VPTCkRE8zkA==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-uri-escape": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-1.0.0-gamma.1.tgz", + "integrity": "sha512-HaTwnGMOFCAC3cqRpI0Mzw4YbCLWY22+n9bbkdw9u65N5JVGQU7E5WSueRBpZUcAkTMwHaYhRzLxdssWs0wwsw==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-5vIAR65+dDq4OyEff44GIg/egd1nUQG5aUu7Hd67FgB0Q7ZDvMIixliFJUlF4S69ryAkjqqbWDKdXl+ofW8utw==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-d8EfFgbfBh2MIV1lJt7rQJ9AV2aYYBdAHbr/rTwVhjn8WfyqQTUXHKPHBdTZrS3yTy3v382QNrnGHxnfykTcXQ==", + "requires": { + "@aws-sdk/types": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-utf8-browser": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-gamma.1.tgz", + "integrity": "sha512-UgHEkgvFvupHR2A4pPofdRflGfZEOPboG7LlUVlH6rcuIJdgi7gTzz4codxOe+kf1PVwHuHR6Pf+t22W6K/WWA==", + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-utf8-node": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-1.0.0-gamma.1.tgz", + "integrity": "sha512-M/bghdcRoquo7/5HdNV7v+Qw3OaSxCOEaRUNZKYekpRXRG2DuDp3EEKX45V/OmBRKWJjDX5DTBnJlo8rev5AUQ==", + "requires": { + "@aws-sdk/util-buffer-from": "1.0.0-gamma.1", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/xml-builder": { + "version": "1.0.0-gamma.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-1.0.0-gamma.1.tgz", + "integrity": "sha512-ttEyfgJXWikHX6ymzCEADI/1IFog5IU/s8jLb6GDGgBaBcxgMI/gJqj3Juzer9rNnUuupyfmAFGD7zH2Mb2hzg==", + "requires": { + "tslib": "^1.8.0" + } + }, "@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", @@ -1479,6 +2841,38 @@ "lodash.deburr": "^4.1.0" } }, + "@sinonjs/commons": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", + "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.2.tgz", + "integrity": "sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==", + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.3.tgz", + "integrity": "sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==", + "requires": { + "@sinonjs/commons": "^1.3.0", + "array-from": "^2.1.1", + "lodash": "^4.17.15" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==" + }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", @@ -2228,6 +3622,28 @@ "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" }, + "amazon-cognito-identity-js": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-4.3.1.tgz", + "integrity": "sha512-Jolhigj6GGFbo5cZ6XZ9ti4IuB8QWQdQeEiwl08JvVYzxR+zYxi/ndcoRSw/Kp81WzNlLVpldvSFjIgstTaTjQ==", + "requires": { + "buffer": "4.9.1", + "crypto-js": "^3.3.0", + "js-cookie": "^2.1.4" + }, + "dependencies": { + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + } + } + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -2421,6 +3837,11 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, "array-includes": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", @@ -2610,6 +4031,25 @@ "postcss-value-parser": "^4.1.0" } }, + "aws-amplify": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/aws-amplify/-/aws-amplify-3.0.17.tgz", + "integrity": "sha512-5odmr8J1+bSm2t5oCPQnI+TC+ND/YVIs6vBhO8hrXSDXV1tiLqdRyTbbSMY+d4phEO5C2WsSWxsrRwhyfWvGTQ==", + "requires": { + "@aws-amplify/analytics": "^3.2.0", + "@aws-amplify/api": "^3.1.16", + "@aws-amplify/auth": "^3.2.13", + "@aws-amplify/cache": "^3.1.16", + "@aws-amplify/core": "^3.3.3", + "@aws-amplify/datastore": "^2.2.3", + "@aws-amplify/interactions": "^3.1.16", + "@aws-amplify/predictions": "^3.1.16", + "@aws-amplify/pubsub": "^3.0.17", + "@aws-amplify/storage": "^3.2.6", + "@aws-amplify/ui": "^2.0.2", + "@aws-amplify/xr": "^2.1.16" + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -4610,6 +6050,11 @@ "randomfill": "^1.0.3" } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -5570,6 +7015,11 @@ } } }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, "diff-sequences": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", @@ -6875,6 +8325,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-xml-parser": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.4.tgz", + "integrity": "sha512-qudnQuyYBgnvzf5Lj/yxMcf4L9NcVWihXJg7CiU1L+oUCq8MUnFEfH2/nXR/W5uq+yvUN1h7z6s7vs2v1WkL1A==" + }, "fastest-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz", @@ -9959,6 +11414,11 @@ } } }, + "idb": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-5.0.2.tgz", + "integrity": "sha512-53yU1RbSPkSkQxufmNgcBkxxnbsTMGaYCv2NwQDmBP75muYj4Z75DsvCqhCCivYcC1XaXDi9tZSUOfDQFxuABA==" + }, "idb-wrapper": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/idb-wrapper/-/idb-wrapper-1.7.2.tgz", @@ -9985,6 +11445,11 @@ "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", "optional": true }, + "immer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-6.0.1.tgz", + "integrity": "sha512-oXwigCKgznQywsXi1VgrqgWbQEU3wievNCVc4Fcwky6mwXU6YHj6JuYp0WEM/B1EphkqsLr0x18lm5OiuemPcA==" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -11351,6 +12816,11 @@ } } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11464,6 +12934,11 @@ "object.assign": "^4.1.0" } }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==" + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -12103,6 +13578,11 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz", "integrity": "sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA==" }, + "lolex": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz", + "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==" + }, "longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", @@ -12820,6 +14300,41 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "nise": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", + "integrity": "sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==", + "requires": { + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, "nlcst-to-string": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-2.0.4.tgz", @@ -13377,6 +14892,11 @@ } } }, + "paho-mqtt": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/paho-mqtt/-/paho-mqtt-1.1.0.tgz", + "integrity": "sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==" + }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -16957,6 +18477,20 @@ "detect-newline": "^1.0.3" } }, + "sinon": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", + "integrity": "sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q==", + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.3", + "diff": "^3.5.0", + "lolex": "^4.2.0", + "nise": "^1.5.2", + "supports-color": "^5.5.0" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -18405,6 +19939,11 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -18450,6 +19989,11 @@ "commander": "~2.20.3" } }, + "ulid": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.3.0.tgz", + "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==" + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -19996,6 +21540,35 @@ } } }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "0.8.19", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz", + "integrity": "sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } + }, + "zen-push": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/zen-push/-/zen-push-0.2.1.tgz", + "integrity": "sha512-Qv4qvc8ZIue51B/0zmeIMxpIGDVhz4GhJALBvnKs/FRa2T7jy4Ori9wFwaHVt0zWV7MIFglKAHbgnVxVTw7U1w==", + "requires": { + "zen-observable": "^0.7.0" + }, + "dependencies": { + "zen-observable": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.7.1.tgz", + "integrity": "sha512-OI6VMSe0yeqaouIXtedC+F55Sr6r9ppS7+wTbSexkYdHbdt4ctTuPNXP/rwm7GTVI63YBc+EBT0b0tl7YnJLRg==" + } + } + }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/docs/package.json b/docs/package.json index 937d04eaa90..b91de202b8e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,9 @@ "build": "gatsby build --prefix-paths" }, "dependencies": { + "@aws-amplify/analytics": "^3.2.0", "antd": "^4.2.4", + "aws-amplify": "^3.0.17", "gatsby": "^2.22.9", "gatsby-plugin-antd": "^2.2.0", "gatsby-plugin-catch-links": "^2.3.2", diff --git a/docs/src/config.js b/docs/src/config.js new file mode 100644 index 00000000000..e158be4bb9a --- /dev/null +++ b/docs/src/config.js @@ -0,0 +1,12 @@ +/* eslint-disable */ + +const awsmobile = { + "aws_project_region": "eu-west-1", + "aws_cognito_identity_pool_id": "eu-west-1:3df3caec-4bb6-4891-b154-ee940c8264b8", + "aws_cognito_region": "eu-west-1", + "aws_kinesis_firehose_stream_name": "ClickStreamKinesisFirehose-OGX7PQdrynUo", + +}; + + +export default awsmobile; From 79b1c6ea6d15855111ca417082ee3a8b3a9f6395 Mon Sep 17 00:00:00 2001 From: heitorlessa Date: Thu, 18 Jun 2020 13:43:04 +0100 Subject: [PATCH 7/8] chore: add missing ':' and identation in examples --- aws_lambda_powertools/metrics/metrics.py | 3 ++- aws_lambda_powertools/tracing/tracer.py | 3 ++- docs/content/core/logger.mdx | 30 ++++++++++++------------ docs/content/core/tracer.mdx | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/aws_lambda_powertools/metrics/metrics.py b/aws_lambda_powertools/metrics/metrics.py index b16f28e92f0..fe4fb559d6f 100644 --- a/aws_lambda_powertools/metrics/metrics.py +++ b/aws_lambda_powertools/metrics/metrics.py @@ -109,7 +109,8 @@ def log_metrics( @tracer.capture_lambda_handler @metrics.log_metrics - def handler(event, context) + def handler(event, context): + ... Parameters ---------- diff --git a/aws_lambda_powertools/tracing/tracer.py b/aws_lambda_powertools/tracing/tracer.py index b97816f25b5..56ba74dfdcf 100644 --- a/aws_lambda_powertools/tracing/tracer.py +++ b/aws_lambda_powertools/tracing/tracer.py @@ -237,7 +237,8 @@ def capture_lambda_handler(self, lambda_handler: Callable[[Dict, Any], Any] = No tracer = Tracer(service="payment") @tracer.capture_lambda_handler - def handler(event, context) + def handler(event, context): + ... Parameters ---------- diff --git a/docs/content/core/logger.mdx b/docs/content/core/logger.mdx index 0fa519420f3..e12c2b7c634 100644 --- a/docs/content/core/logger.mdx +++ b/docs/content/core/logger.mdx @@ -68,15 +68,15 @@ from aws_lambda_powertools import Logger logger = Logger() @logger.inject_lambda_context -def handler(event, context) - logger.info("Collecting payment") - ... - # You can log entire objects too - logger.info({ - "operation": "collect_payment", - "charge_id": event['charge_id'] - }) - ... +def handler(event, context): + logger.info("Collecting payment") + ... + # You can log entire objects too + logger.info({ + "operation": "collect_payment", + "charge_id": event['charge_id'] + }) + ... ``` You can also explicitly log any incoming event using `log_event` param or via `POWERTOOLS_LOGGER_LOG_EVENT` env var. @@ -91,8 +91,8 @@ from aws_lambda_powertools import Logger logger = Logger() @logger.inject_lambda_context(log_event=True) # highlight-start -def handler(event, context) - ... +def handler(event, context): + ... ``` When used, this will include the following keys: @@ -155,8 +155,8 @@ from aws_lambda_powertools import Logger logger = Logger() -def handler(event, context) - if "order_id" in event: +def handler(event, context): + if "order_id" in event: logger.structure_logs(append=True, order_id=event["order_id"]) # highlight-line logger.info("Collecting payment") ... @@ -199,8 +199,8 @@ from aws_lambda_powertools import Logger # Sample 1% of debug logs e.g. 0.1 logger = Logger(sample_rate=0.1) # highlight-line -def handler(event, context) - if "order_id" in event: +def handler(event, context): + if "order_id" in event: logger.info("Collecting payment") ... ``` diff --git a/docs/content/core/tracer.mdx b/docs/content/core/tracer.mdx index a4a4fbff658..ba489b0c965 100644 --- a/docs/content/core/tracer.mdx +++ b/docs/content/core/tracer.mdx @@ -59,7 +59,7 @@ from aws_lambda_powertools import Tracer tracer = Tracer() @tracer.capture_lambda_handler # highlight-line -def handler(event, context) +def handler(event, context): charge_id = event.get('charge_id') payment = collect_payment(charge_id) ... @@ -205,7 +205,7 @@ from aws_lambda_powertools import Tracer tracer = Tracer(service="payment") @tracer.capture_lambda_handler -def handler(event, context) +def handler(event, context): charge_id = event.get('charge_id') payment = collect_payment(charge_id) ... From 21618232c86e1df3ebe71211709b9a5156bae552 Mon Sep 17 00:00:00 2001 From: Heitor Lessa Date: Thu, 18 Jun 2020 16:45:16 +0100 Subject: [PATCH 8/8] chore: bump to 1.0.0 GA (#83) * chore: bump version and remove beta/RC status Signed-off-by: heitorlessa * docs: add quick helpful links to the top Signed-off-by: heitorlessa * fix: metadata typo Co-authored-by: Tom McCarthy Co-authored-by: Tom McCarthy --- CHANGELOG.md | 5 +++++ README.md | 4 +--- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75c257c3bf9..c924e2a8f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] - 2020-06-18 +### Added +- **Metrics**: `add_metadata` method to add any metric metadata you'd like to ease finding metric related data via CloudWatch Logs +- Set status as General Availability + ## [0.11.0] - 2020-06-08 ### Added - Imports can now be made from top level of module, e.g.: `from aws_lambda_powertools import Logger, Metrics, Tracer` diff --git a/README.md b/README.md index f334dde0324..18de8e00e0d 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,7 @@ A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating custom metrics asynchronously easier. -* **Status**: Release candidate -* **How long until GA?**: [Current progress](https://github.com/awslabs/aws-lambda-powertools/projects/1) -* **Docs**: [Main docs](https://awslabs.github.io/aws-lambda-powertools-python/) | [API Docs](https://awslabs.github.io/aws-lambda-powertools-python/api/) +**[📜Documentation](https://awslabs.github.io/aws-lambda-powertools-python/)** | **[API Docs](https://awslabs.github.io/aws-lambda-powertools-python/api/)** | **[🐍PyPi](https://pypi.org/project/aws-lambda-powertools/)** | **[Feature request](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=feature-request%2C+triage&template=feature_request.md&title=)** | **[🐛Bug Report](https://github.com/awslabs/aws-lambda-powertools-python/issues/new?assignees=&labels=bug%2C+triage&template=bug_report.md&title=)** | **[Kitchen sink example](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/example)** ## Features diff --git a/pyproject.toml b/pyproject.toml index c147ab1c9a7..79fc7753f07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aws_lambda_powertools" -version = "0.11.0" +version = "1.0.0" description = "Python utilities for AWS Lambda functions including but not limited to tracing, logging and custom metric" authors = ["Amazon Web Services"] classifiers=[