From c3a20eca17acc543b9d1f236ad03d34a18c63feb Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Tue, 14 May 2024 07:23:47 -0700 Subject: [PATCH 01/38] add some debug around report generation --- app/notifications/rest.py | 12 +++++++----- app/service/rest.py | 7 +++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 55d3c101a..34c70417e 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -64,7 +64,9 @@ def get_notification_by_id(notification_id): @notifications.route("/notifications", methods=["GET"]) def get_all_notifications(): + current_app.logger.debug(f"enter get_all_notifications()") data = notifications_filter_schema.load(request.args) + current_app.logger.debug(f"get_all_notifications() data {data} request.args {request.args}") include_jobs = data.get("include_jobs", False) page = data.get("page", 1) @@ -96,8 +98,7 @@ def get_all_notifications(): notification.to = recipient notification.normalised_to = recipient - return ( - jsonify( + result = jsonify( notifications=notification_with_personalisation_schema.dump( pagination.items, many=True ), @@ -106,9 +107,10 @@ def get_all_notifications(): links=pagination_links( pagination, ".get_all_notifications", **request.args.to_dict() ), - ), - 200, - ) + ) + current_app.logger.debug(f"result={result}") + return result,200 + @notifications.route("/notifications/", methods=["POST"]) diff --git a/app/service/rest.py b/app/service/rest.py index ce5083073..9a6aa7f3d 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -389,12 +389,15 @@ def get_service_history(service_id): @service_blueprint.route("//notifications", methods=["GET", "POST"]) def get_all_notifications_for_service(service_id): + current_app.logger.debug(f"enter get_all_notifications_for_service") if request.method == "GET": data = notifications_filter_schema.load(request.args) + current_app.logger.debug(f"use GET, request.args {request.args} and data {data}") elif request.method == "POST": # Must transform request.get_json() to MultiDict as NotificationsFilterSchema expects a MultiDict. # Unlike request.args, request.get_json() does not return a MultiDict but instead just a dict. data = notifications_filter_schema.load(MultiDict(request.get_json())) + current_app.logger.debug(f"use POST, request {request.get_json()} data {data}") if data.get("to"): notification_type = ( @@ -421,6 +424,8 @@ def get_all_notifications_for_service(service_id): # for whether to show pagination links count_pages = data.get("count_pages", True) + current_app.logger.debug(f"get pagination with {service_id} service_id filters {data} \ + limit_days {limit_days} include_jobs {include_jobs} include_one_off {include_one_off}") pagination = notifications_dao.get_notifications_for_service( service_id, filter_dict=data, @@ -462,6 +467,8 @@ def get_all_notifications_for_service(service_id): notifications = notification_with_template_schema.dump( pagination.items, many=True ) + current_app.logger.debug(f"number of notifications are {len(notifications)}") + current_app.logger.debug(f"first notification is {notifications[0]}") # We try and get the next page of results to work out if we need provide a pagination link to the next page # in our response if it exists. Note, this could be done instead by changing `count_pages` in the previous # call to be True which will enable us to use Flask-Sqlalchemy to tell if there is a next page of results but From 9d95d8d653d1b76a2575b3bb938896990f105789 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 16:08:04 +0000 Subject: [PATCH 02/38] Bump faker from 25.1.0 to 25.2.0 Bumps [faker](https://github.com/joke2k/faker) from 25.1.0 to 25.2.0. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v25.1.0...v25.2.0) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6c9addcb1..f12167fca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1241,13 +1241,13 @@ tests = ["coverage", "coveralls", "dill", "mock", "nose"] [[package]] name = "faker" -version = "25.1.0" +version = "25.2.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-25.1.0-py3-none-any.whl", hash = "sha256:24e28dce0b89683bb9e017e042b971c8c4909cff551b6d46f1e207674c7c2526"}, - {file = "Faker-25.1.0.tar.gz", hash = "sha256:2107618cf306bb188dcfea3e5cfd94aa92d65c7293a2437c1e96a99c83274755"}, + {file = "Faker-25.2.0-py3-none-any.whl", hash = "sha256:cfe97c4857c4c36ee32ea4aaabef884895992e209bae4cbd26807cf3e05c6918"}, + {file = "Faker-25.2.0.tar.gz", hash = "sha256:45b84f47ff1ef86e3d1a8d11583ca871ecf6730fad0660edadc02576583a2423"}, ] [package.dependencies] @@ -4815,4 +4815,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "1a02dcc6b7781892e503f1a7eeed975970974243e94d2a3afaec9b6a8369f08c" +content-hash = "1210f9b0a4fb3a7d0e8f29bc2a37fad77b042a1cec1721e8e3608bb280658f3f" diff --git a/pyproject.toml b/pyproject.toml index e3ba50a14..b3d9de437 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ pyjwt = "==2.8.0" python-dotenv = "==1.0.1" sqlalchemy = "==2.0.30" werkzeug = "^3.0.3" -faker = "^25.1.0" +faker = "^25.2.0" [tool.poetry.group.dev.dependencies] From 3cc0f21b4714ecf5deafc653e70438a3e4bada7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 18:13:45 +0000 Subject: [PATCH 03/38] Bump moto from 5.0.6 to 5.0.7 Bumps [moto](https://github.com/getmoto/moto) from 5.0.6 to 5.0.7. - [Release notes](https://github.com/getmoto/moto/releases) - [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md) - [Commits](https://github.com/getmoto/moto/compare/5.0.6...5.0.7) --- updated-dependencies: - dependency-name: moto dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 26 +++++++++++++------------- pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/poetry.lock b/poetry.lock index f12167fca..a0474b8da 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2381,13 +2381,13 @@ files = [ [[package]] name = "moto" -version = "5.0.6" +version = "5.0.7" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.6-py2.py3-none-any.whl", hash = "sha256:ca1e22831a741733b581ff2ef4d6ae2e1c6db1eab97af1b78b86ca2c6e88c609"}, - {file = "moto-5.0.6.tar.gz", hash = "sha256:ad8b23f2b555ad694da8b2432a42b6d96beaaf67a4e7d932196a72193a2eee2c"}, + {file = "moto-5.0.7-py2.py3-none-any.whl", hash = "sha256:c0214c1361fb1dc85f587d9ce17cd988c6f69ff0ed54d43789654022e0e744f2"}, + {file = "moto-5.0.7.tar.gz", hash = "sha256:f2cde691dc4bc675e318a65f018902ac7f89d61bf2646052f7df215d212f069e"}, ] [package.dependencies] @@ -2402,23 +2402,23 @@ werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" xmltodict = "*" [package.extras] -all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.4)", "pyparsing (>=3.0.7)", "setuptools"] +all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"] apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"] appsync = ["graphql-core"] awslambda = ["docker (>=3.0.0)"] batch = ["docker (>=3.0.0)"] -cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.4)", "pyparsing (>=3.0.7)", "setuptools"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] cognitoidp = ["joserfc (>=0.9.0)"] -dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.4)"] -dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.4)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.5)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.5)"] glue = ["pyparsing (>=3.0.7)"] iotdata = ["jsondiff (>=1.1.2)"] -proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.4)", "pyparsing (>=3.0.7)", "setuptools"] -resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.4)", "pyparsing (>=3.0.7)"] -s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.4)"] -s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.4)"] -server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.4)", "pyparsing (>=3.0.7)", "setuptools"] +proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.5)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.5)"] +server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.5)", "pyparsing (>=3.0.7)", "setuptools"] ssm = ["PyYAML (>=5.1)"] stepfunctions = ["antlr4-python3-runtime", "jsonpath-ng"] xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] @@ -4815,4 +4815,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "1210f9b0a4fb3a7d0e8f29bc2a37fad77b042a1cec1721e8e3608bb280658f3f" +content-hash = "4a100f7304bda578329c1f412adb483b01d1741bf66bfafeaaa979bf36a2c92d" diff --git a/pyproject.toml b/pyproject.toml index b3d9de437..b7973eb1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ freezegun = "^1.5.1" honcho = "*" isort = "^5.13.2" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} -moto = "==5.0.6" +moto = "==5.0.7" pip-audit = "*" pre-commit = "^3.6.0" pytest = "^8.1.1" From c069121c7a7a2fc73d3a39062a4a22f6dfbc628d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 19:13:07 +0000 Subject: [PATCH 04/38] Bump pre-commit from 3.7.0 to 3.7.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.7.0 to 3.7.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.7.0...v3.7.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index a0474b8da..ae7fc5bcc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3092,13 +3092,13 @@ files = [ [[package]] name = "pre-commit" -version = "3.7.0" +version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, - {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] @@ -4815,4 +4815,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "4a100f7304bda578329c1f412adb483b01d1741bf66bfafeaaa979bf36a2c92d" +content-hash = "c43c8759e9259fb87f001a17d32681caf84f0d90b740c4fae61e23a08a64b202" diff --git a/pyproject.toml b/pyproject.toml index b7973eb1e..7c2f25ce7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ isort = "^5.13.2" jinja2-cli = {version = "==0.8.2", extras = ["yaml"]} moto = "==5.0.7" pip-audit = "*" -pre-commit = "^3.6.0" +pre-commit = "^3.7.1" pytest = "^8.1.1" pytest-env = "^1.1.3" pytest-mock = "^3.14.0" From 6a940ba20985761c7c43ef779b7169fca17106ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 21:58:39 +0000 Subject: [PATCH 05/38] Bump boto3 from 1.34.101 to 1.34.103 Bumps [boto3](https://github.com/boto/boto3) from 1.34.101 to 1.34.103. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.34.101...1.34.103) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index ae7fc5bcc..5bdc9b5bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -403,17 +403,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.101" +version = "1.34.103" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.101-py3-none-any.whl", hash = "sha256:79b93f3370ea96ce838042bc2eac0c996aee204b01e7e6452eb77abcbe697d6a"}, - {file = "boto3-1.34.101.tar.gz", hash = "sha256:1d854b5880e185db546b4c759fcb664bf3326275064d2b44229cc217e8be9d7e"}, + {file = "boto3-1.34.103-py3-none-any.whl", hash = "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365"}, + {file = "boto3-1.34.103.tar.gz", hash = "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9"}, ] [package.dependencies] -botocore = ">=1.34.101,<1.35.0" +botocore = ">=1.34.103,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -4815,4 +4815,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "c43c8759e9259fb87f001a17d32681caf84f0d90b740c4fae61e23a08a64b202" +content-hash = "477254951a3f060f16823c1c5891091efa2f86d0406debe0843e62bb4330116b" diff --git a/pyproject.toml b/pyproject.toml index 7c2f25ce7..10cfcdd6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.12.2" alembic = "==1.13.1" amqp = "==5.2.0" beautifulsoup4 = "==4.12.3" -boto3 = "^1.34.101" +boto3 = "^1.34.103" botocore = "^1.34.103" cachetools = "==5.3.3" celery = {version = "==5.3.6", extras = ["redis"]} From 92d4a3d81686fab4a06c9402e1419e9bdb03737f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 13:58:37 +0000 Subject: [PATCH 06/38] Bump botocore from 1.34.103 to 1.34.105 Bumps [botocore](https://github.com/boto/botocore) from 1.34.103 to 1.34.105. - [Changelog](https://github.com/boto/botocore/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/botocore/compare/1.34.103...1.34.105) --- updated-dependencies: - dependency-name: botocore dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5bdc9b5bb..8bd89c644 100644 --- a/poetry.lock +++ b/poetry.lock @@ -204,17 +204,17 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "awscli" -version = "1.32.103" +version = "1.32.105" description = "Universal Command Line Environment for AWS." optional = false python-versions = ">=3.8" files = [ - {file = "awscli-1.32.103-py3-none-any.whl", hash = "sha256:443b6c10c93a6f2e632d3e9572d803c1ce015388c457d3f5ca198e0c265f71d1"}, - {file = "awscli-1.32.103.tar.gz", hash = "sha256:9edbcd613d8dd858ca951a412fe0e28ac5d6b8ea538601963b9d57c8d11b5b10"}, + {file = "awscli-1.32.105-py3-none-any.whl", hash = "sha256:46820755a670fe585767eaf9b131474e5edd9a8de6478f4fcacff65e2d41b780"}, + {file = "awscli-1.32.105.tar.gz", hash = "sha256:560126f736ff71844b0a89809a77fb275319af82f18ae6678cfa9c1c5ee1d24c"}, ] [package.dependencies] -botocore = "1.34.103" +botocore = "1.34.105" colorama = ">=0.2.5,<0.4.7" docutils = ">=0.10,<0.17" PyYAML = ">=3.10,<6.1" @@ -422,13 +422,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.103" +version = "1.34.105" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.103-py3-none-any.whl", hash = "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431"}, - {file = "botocore-1.34.103.tar.gz", hash = "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939"}, + {file = "botocore-1.34.105-py3-none-any.whl", hash = "sha256:a459d060b541beecb50681e6e8a39313cca981e146a59ba7c5229d62f631a016"}, + {file = "botocore-1.34.105.tar.gz", hash = "sha256:727d5d3e800ac8b705fca6e19b6fefa1e728a81d62a712df9bd32ed0117c740b"}, ] [package.dependencies] @@ -4815,4 +4815,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "477254951a3f060f16823c1c5891091efa2f86d0406debe0843e62bb4330116b" +content-hash = "329b1aeceb5284b65e2eb1df563a12f2acf2e9f4f655848be64113cc1090aa79" diff --git a/pyproject.toml b/pyproject.toml index 10cfcdd6b..487cb2088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ alembic = "==1.13.1" amqp = "==5.2.0" beautifulsoup4 = "==4.12.3" boto3 = "^1.34.103" -botocore = "^1.34.103" +botocore = "^1.34.105" cachetools = "==5.3.3" celery = {version = "==5.3.6", extras = ["redis"]} certifi = ">=2022.12.7" From 9f36b49027cf771188aa6a2c7b05798f0204ddfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 15:00:47 +0000 Subject: [PATCH 07/38] Bump lxml from 5.2.1 to 5.2.2 Bumps [lxml](https://github.com/lxml/lxml) from 5.2.1 to 5.2.2. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-5.2.1...lxml-5.2.2) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 296 +++++++++++++++++++++++-------------------------- pyproject.toml | 2 +- 2 files changed, 141 insertions(+), 157 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8bd89c644..fe9980cf2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1999,165 +1999,149 @@ testing = ["black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twin [[package]] name = "lxml" -version = "5.2.1" +version = "5.2.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1"}, - {file = "lxml-5.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a"}, - {file = "lxml-5.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01"}, - {file = "lxml-5.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1"}, - {file = "lxml-5.2.1-cp310-cp310-win32.whl", hash = "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5"}, - {file = "lxml-5.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, - {file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, - {file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, - {file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, - {file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, - {file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, - {file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, - {file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"}, - {file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"}, - {file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"}, - {file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"}, - {file = "lxml-5.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863"}, - {file = "lxml-5.2.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62"}, - {file = "lxml-5.2.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461"}, - {file = "lxml-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0"}, - {file = "lxml-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289"}, - {file = "lxml-5.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0"}, - {file = "lxml-5.2.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75"}, - {file = "lxml-5.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8"}, - {file = "lxml-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd"}, - {file = "lxml-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c"}, - {file = "lxml-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533"}, - {file = "lxml-5.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c"}, - {file = "lxml-5.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637"}, - {file = "lxml-5.2.1-cp38-cp38-win32.whl", hash = "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da"}, - {file = "lxml-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd"}, - {file = "lxml-5.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b"}, - {file = "lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c"}, - {file = "lxml-5.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188"}, - {file = "lxml-5.2.1-cp39-cp39-win32.whl", hash = "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708"}, - {file = "lxml-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"}, - {file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"}, - {file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"}, - {file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"}, - {file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"}, - {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632"}, + {file = "lxml-5.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f"}, + {file = "lxml-5.2.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393"}, + {file = "lxml-5.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526"}, + {file = "lxml-5.2.2-cp310-cp310-win32.whl", hash = "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30"}, + {file = "lxml-5.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"}, + {file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"}, + {file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"}, + {file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"}, + {file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"}, + {file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"}, + {file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"}, + {file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"}, + {file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"}, + {file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"}, + {file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"}, + {file = "lxml-5.2.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce"}, + {file = "lxml-5.2.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9"}, + {file = "lxml-5.2.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264"}, + {file = "lxml-5.2.2-cp36-cp36m-win32.whl", hash = "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3"}, + {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, + {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, + {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, + {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, + {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, + {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, + {file = "lxml-5.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5"}, + {file = "lxml-5.2.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b"}, + {file = "lxml-5.2.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1"}, + {file = "lxml-5.2.2-cp38-cp38-win32.whl", hash = "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30"}, + {file = "lxml-5.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30"}, + {file = "lxml-5.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a"}, + {file = "lxml-5.2.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472"}, + {file = "lxml-5.2.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9"}, + {file = "lxml-5.2.2-cp39-cp39-win32.whl", hash = "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf"}, + {file = "lxml-5.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"}, + {file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"}, + {file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"}, + {file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"}, + {file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"}, + {file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"}, ] [package.extras] @@ -4815,4 +4799,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "329b1aeceb5284b65e2eb1df563a12f2acf2e9f4f655848be64113cc1090aa79" +content-hash = "1db210ff3b08c52ae4e29b7a6522e347eca510332387fb1f0ed08b708410cfc3" diff --git a/pyproject.toml b/pyproject.toml index 487cb2088..556443cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ flask-sqlalchemy = "==3.1.1" gunicorn = {version = "==22.0.0", extras = ["eventlet"]} iso8601 = "==2.1.0" jsonschema = {version = "==4.22.0", extras = ["format"]} -lxml = "==5.2.1" +lxml = "==5.2.2" marshmallow = "==3.21.2" marshmallow-sqlalchemy = "==1.0.0" newrelic = "*" From ac6acba059155e85abcdb30b0bd43347efeed004 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 15:31:58 +0000 Subject: [PATCH 08/38] Bump newrelic from 9.9.0 to 9.9.1 Bumps [newrelic](https://github.com/newrelic/newrelic-python-agent) from 9.9.0 to 9.9.1. - [Release notes](https://github.com/newrelic/newrelic-python-agent/releases) - [Commits](https://github.com/newrelic/newrelic-python-agent/compare/v9.9.0...v9.9.1) --- updated-dependencies: - dependency-name: newrelic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index fe9980cf2..f96972e2a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2584,40 +2584,40 @@ files = [ [[package]] name = "newrelic" -version = "9.9.0" +version = "9.9.1" description = "New Relic Python Agent" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "newrelic-9.9.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:db32fa04d69bbb742401c124a6cec158e6237a21af4602dbf53e4630ea9dd068"}, - {file = "newrelic-9.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9dbf35914d0bbf1294d8eb6fa5357d072238c6c722726c2ee20b9c1e35b8253d"}, - {file = "newrelic-9.9.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e6cb86aa2f7230ee9dcb5f9f8821c7090566419def5537a44240f978b680c4f7"}, - {file = "newrelic-9.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a91dea75f8c202a6a553339a1997983224465555a3f8d7294b24de1e2bee5f05"}, - {file = "newrelic-9.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dac3b74bd801513e8221f05a01a294405eda7f4922fce5b174e5e33c222ae09d"}, - {file = "newrelic-9.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a257995d832858cf7c56bcfb1911f3379f9d3e795d7357f56f035f1b60339ea0"}, - {file = "newrelic-9.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04cd3fc7087513a4786908a9b0a7475db154c888ac9d2de251f8abb93353a4a7"}, - {file = "newrelic-9.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:26713f779cf23bb29c6b408436167059d0c8ee1475810dc1b0efe858fe578f25"}, - {file = "newrelic-9.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf3c13d264cd089d467e9848fb6875907940202d22475b506a70683f04ef82af"}, - {file = "newrelic-9.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a57ff176818037983589c15b6dca03841fcef1429c279f5948800caa333fb476"}, - {file = "newrelic-9.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:63b230dd5d093874c0137eddc738cb028e17326d2a8a98cbc12c665bbdf6ec67"}, - {file = "newrelic-9.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4cf5d85a4a8e8de6e0aeb7a76afad9264d0c0dc459bc3f1a8b02a0e48a9a26da"}, - {file = "newrelic-9.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de2ac509f8730fc6f6819f13a9ebbe52865397d526ca4dbe963a0e9865bb0500"}, - {file = "newrelic-9.9.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8304317ff27bb50fd94f1e6e8c3ae0c59151ee85de2ea0269dbe7e982512c45"}, - {file = "newrelic-9.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b773ee74d869bf632ce1e12903cc8e7ae8b5697ef9ae97169ed263a5d3a87f76"}, - {file = "newrelic-9.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4356690cbc9e5e662defa2af15aba05901cf9b285a8d02aeb90718e84dd6d779"}, - {file = "newrelic-9.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e12ead3602ca2c188528fde444f8ab953b504b095d70265303bbf132908eb7"}, - {file = "newrelic-9.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b64a61f2f228b70f91c06a0bd82e2645c6b75ddbd50587f94a67c89ef6d5d854"}, - {file = "newrelic-9.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b60f66132a42ec8c67fd26b8082cc3a0626192283dc9b5716a66203a58f10d30"}, - {file = "newrelic-9.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:834ce8de7550bc444aed6c2afc1436c04485998e46f429e41b89d66ab85f0fbb"}, - {file = "newrelic-9.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57451807f600331a94ad1ec66e3981523b0516d5b2dd9fd078e7f3d6c9228913"}, - {file = "newrelic-9.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f48898e268dcaa14aa1b6d5c8b8d10f3f4396589a37be10a06bb5ba262ef0541"}, - {file = "newrelic-9.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2ffcbdb706de1bbaa36acd0c9b487a08895a420020bcf775be2d80c7df29b56c"}, - {file = "newrelic-9.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5b40155f9712e75c00d03cdec8272f6cf8eaa05ea2ed22bb5ecc96ed86017b47"}, - {file = "newrelic-9.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47efe8fc4dc14b0f265d635639f94ef5a071b5e5ebbf41ecf0946fce071c49e6"}, - {file = "newrelic-9.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6198259dae01212b39079add58e0ef7311cf01734adea51fec4d2f7a9fafec"}, - {file = "newrelic-9.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0d8c8f66aba3629f0f17a1d2314beb2984ad7c485dd318ef2d5f257c040981d"}, - {file = "newrelic-9.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1743df0e72bf559b61112763a71c35e5d456a509ba4dde2bdbaa88d894f1812a"}, - {file = "newrelic-9.9.0.tar.gz", hash = "sha256:2182673a01f04a0ed4a0bb3f49e8fa869044c37558c8f409c96de13105f58a57"}, + {file = "newrelic-9.9.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:474499f482da7f58b5039f2c42dea2880d878b30729ae563bb1498a0bb30be44"}, + {file = "newrelic-9.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3c99cc368a3cfd9ce40ca4bbe2fe3bdd5f7d37865ea5e4bf811ba6fd0d00152d"}, + {file = "newrelic-9.9.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3ef567a779b068297c040f7410153135fb12e51e4a82084675b0cf142c407551"}, + {file = "newrelic-9.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:303117d3402659afac45174dfe7c595b7d4b3c0812a76b712c251c91ef95c430"}, + {file = "newrelic-9.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c813e9c7bdb1381cb0eda4925e07aa8ee21e111b5025d02261605eaabb129f1"}, + {file = "newrelic-9.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5d688917307d083d7fa6f3b31eec40c5a3782b160383230f5f644e2d4ae2a26"}, + {file = "newrelic-9.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5710910ceb847f8806540e6934764fff6823d7dcc6d30955e9ecb012e20efbfd"}, + {file = "newrelic-9.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:aefa66f59d62ec22a6d347afa73c24bd723521c4cc0fdce7f51c71bfe85c42bc"}, + {file = "newrelic-9.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afdb30c4f89d0f089ac05ca50a383f94cfcdb07aab0b9722d2d5af09626ab304"}, + {file = "newrelic-9.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6361af2a60ab60a5757b13ce0b9b4efeee577a228637b9b8b449d47ec81fdd"}, + {file = "newrelic-9.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7aa1be0d0530d0c566dee2c4d43765aba9fc5fae256fac110ba57aae6ae8d8c4"}, + {file = "newrelic-9.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8ad34b8eb60f33b0eab9ed7727cdb9452ad7d4381a2c5397e6ed3d4895833fd1"}, + {file = "newrelic-9.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e613f1ffd0d35b1f866382eeee52d8aa9576d82f3de818a84aa2e56c08f1868"}, + {file = "newrelic-9.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3264e305ae0e973f3a02f7394460f4c7366822e8a3509cd08b2093f9cb5def5"}, + {file = "newrelic-9.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2b165328c05fd2c006cf1f476bebb281579944418a13903e802344660b13332c"}, + {file = "newrelic-9.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e3226ac2c0c57955a00a11f6cf982dd6747490254ed322d6fcf36077bfc37386"}, + {file = "newrelic-9.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673ed069516fa4d168cd12b7319bcadf75fbc9f0ebcd147916e281b2bc16c551"}, + {file = "newrelic-9.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40820a3dff89cc8e242f0543fabd1692333458f627ebad6f2e56f6c9db7d2efe"}, + {file = "newrelic-9.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ddb2d4a2fc3f88c5d1c0b4dec2f8eb89907541501f2ec7ac14e5506ea702e0f5"}, + {file = "newrelic-9.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d50fa347584967c15e574a2503fdcafcd13c86c17e589021eae5432d4aad1cca"}, + {file = "newrelic-9.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbca7a8749eadb05eacdfb68af938dc1045c6be8bcc83375d15a840172b5f40e"}, + {file = "newrelic-9.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d6feba8968662c7a84ee6fe837d3be8c53a7126398ded3283634bb51dc43e94"}, + {file = "newrelic-9.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:eec85620708aea387b602db61fb43504efc5b5fcb7b627d2cbe0a33c3fe10ab9"}, + {file = "newrelic-9.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:21e280c027835062f54be2df48f32834dcc98f382b049c14ee35b80aa7b48ea0"}, + {file = "newrelic-9.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb0e56324df855c3079d7d86fd6b35e79727759de8c8517be9c06d482092c3b"}, + {file = "newrelic-9.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c43a14c48dd8f752da348c3ec80cb500b9ead12abcd40d29d39a0bb8a62a3a0d"}, + {file = "newrelic-9.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:763faab4868b0226906c17ef0419dab527964f489cb2e3818d57d0484762cb2e"}, + {file = "newrelic-9.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7f41343548aad28b7722c85d00079b4e61ef48d5a6bdf757c458a5fe860bb099"}, + {file = "newrelic-9.9.1.tar.gz", hash = "sha256:e49c734058c7b6a6c199e8c2657187143061a6eda92cc8ba67739de88a9e203d"}, ] [package.extras] From e7c68531f261704708f0c520e6583805057f031f Mon Sep 17 00:00:00 2001 From: Kenneth Kehl <@kkehl@flexion.us> Date: Wed, 15 May 2024 08:34:56 -0700 Subject: [PATCH 09/38] fix flake 8 --- app/notifications/rest.py | 27 ++++++++++++++------------- app/service/rest.py | 12 ++++++++---- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 34c70417e..964bee19b 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -64,9 +64,11 @@ def get_notification_by_id(notification_id): @notifications.route("/notifications", methods=["GET"]) def get_all_notifications(): - current_app.logger.debug(f"enter get_all_notifications()") + current_app.logger.debug("enter get_all_notifications()") data = notifications_filter_schema.load(request.args) - current_app.logger.debug(f"get_all_notifications() data {data} request.args {request.args}") + current_app.logger.debug( + f"get_all_notifications() data {data} request.args {request.args}" + ) include_jobs = data.get("include_jobs", False) page = data.get("page", 1) @@ -99,18 +101,17 @@ def get_all_notifications(): notification.normalised_to = recipient result = jsonify( - notifications=notification_with_personalisation_schema.dump( - pagination.items, many=True - ), - page_size=page_size, - total=pagination.total, - links=pagination_links( - pagination, ".get_all_notifications", **request.args.to_dict() - ), - ) + notifications=notification_with_personalisation_schema.dump( + pagination.items, many=True + ), + page_size=page_size, + total=pagination.total, + links=pagination_links( + pagination, ".get_all_notifications", **request.args.to_dict() + ), + ) current_app.logger.debug(f"result={result}") - return result,200 - + return result, 200 @notifications.route("/notifications/", methods=["POST"]) diff --git a/app/service/rest.py b/app/service/rest.py index 9a6aa7f3d..70c6e910d 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -389,10 +389,12 @@ def get_service_history(service_id): @service_blueprint.route("//notifications", methods=["GET", "POST"]) def get_all_notifications_for_service(service_id): - current_app.logger.debug(f"enter get_all_notifications_for_service") + current_app.logger.debug("enter get_all_notifications_for_service") if request.method == "GET": data = notifications_filter_schema.load(request.args) - current_app.logger.debug(f"use GET, request.args {request.args} and data {data}") + current_app.logger.debug( + f"use GET, request.args {request.args} and data {data}" + ) elif request.method == "POST": # Must transform request.get_json() to MultiDict as NotificationsFilterSchema expects a MultiDict. # Unlike request.args, request.get_json() does not return a MultiDict but instead just a dict. @@ -424,8 +426,10 @@ def get_all_notifications_for_service(service_id): # for whether to show pagination links count_pages = data.get("count_pages", True) - current_app.logger.debug(f"get pagination with {service_id} service_id filters {data} \ - limit_days {limit_days} include_jobs {include_jobs} include_one_off {include_one_off}") + current_app.logger.debug( + f"get pagination with {service_id} service_id filters {data} \ + limit_days {limit_days} include_jobs {include_jobs} include_one_off {include_one_off}" + ) pagination = notifications_dao.get_notifications_for_service( service_id, filter_dict=data, From 39fd6f5c3076c073c9de41a8e7a43b90e7301d61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 15:49:50 +0000 Subject: [PATCH 10/38] Bump celery from 5.3.6 to 5.4.0 Bumps [celery](https://github.com/celery/celery) from 5.3.6 to 5.4.0. - [Release notes](https://github.com/celery/celery/releases) - [Changelog](https://github.com/celery/celery/blob/main/Changelog.rst) - [Commits](https://github.com/celery/celery/compare/v5.3.6...v5.4.0) --- updated-dependencies: - dependency-name: celery dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 21 +++++++++++---------- pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/poetry.lock b/poetry.lock index f96972e2a..42d4046f7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -496,13 +496,13 @@ files = [ [[package]] name = "celery" -version = "5.3.6" +version = "5.4.0" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" files = [ - {file = "celery-5.3.6-py3-none-any.whl", hash = "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"}, - {file = "celery-5.3.6.tar.gz", hash = "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9"}, + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, ] [package.dependencies] @@ -519,7 +519,7 @@ vine = ">=5.1.0,<6.0" [package.extras] arangodb = ["pyArango (>=2.0.2)"] -auth = ["cryptography (==41.0.5)"] +auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] @@ -529,22 +529,23 @@ couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] -elasticsearch = ["elastic-transport (<=8.10.0)", "elasticsearch (<=8.11.0)"] +elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] eventlet = ["eventlet (>=0.32.0)"] +gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] librabbitmq = ["librabbitmq (>=2.0.0)"] memcache = ["pylibmc (==1.6.3)"] mongodb = ["pymongo[srv] (>=4.0.2)"] -msgpack = ["msgpack (==1.0.7)"] -pymemcache = ["python-memcached (==1.59)"] +msgpack = ["msgpack (==1.0.8)"] +pymemcache = ["python-memcached (>=1.61)"] pyro = ["pyro4 (==4.82)"] -pytest = ["pytest-celery (==0.0.0)"] +pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] solar = ["ephem (==4.1.5)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] @@ -4799,4 +4800,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "1db210ff3b08c52ae4e29b7a6522e347eca510332387fb1f0ed08b708410cfc3" +content-hash = "1462eb2579412b905ba0a19cbb2bc2020953963b28f9516e318a425a4a3aa3b2" diff --git a/pyproject.toml b/pyproject.toml index 556443cb0..1edf5516c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ beautifulsoup4 = "==4.12.3" boto3 = "^1.34.103" botocore = "^1.34.105" cachetools = "==5.3.3" -celery = {version = "==5.3.6", extras = ["redis"]} +celery = {version = "==5.4.0", extras = ["redis"]} certifi = ">=2022.12.7" cffi = "==1.16.0" charset-normalizer = "^3.1.0" From 4e2d71f8a3f3af05c291ce12619581c9c223e878 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 21:06:11 +0000 Subject: [PATCH 11/38] Bump botocore from 1.34.105 to 1.34.106 Bumps [botocore](https://github.com/boto/botocore) from 1.34.105 to 1.34.106. - [Changelog](https://github.com/boto/botocore/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/botocore/compare/1.34.105...1.34.106) --- updated-dependencies: - dependency-name: botocore dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 16 ++++++++-------- pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 42d4046f7..b7c783cad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -204,17 +204,17 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "awscli" -version = "1.32.105" +version = "1.32.106" description = "Universal Command Line Environment for AWS." optional = false python-versions = ">=3.8" files = [ - {file = "awscli-1.32.105-py3-none-any.whl", hash = "sha256:46820755a670fe585767eaf9b131474e5edd9a8de6478f4fcacff65e2d41b780"}, - {file = "awscli-1.32.105.tar.gz", hash = "sha256:560126f736ff71844b0a89809a77fb275319af82f18ae6678cfa9c1c5ee1d24c"}, + {file = "awscli-1.32.106-py3-none-any.whl", hash = "sha256:32f050c2c5f73c0be2eb71449aba7bb6db2c9569ea1c4f28357389f5acbb5a82"}, + {file = "awscli-1.32.106.tar.gz", hash = "sha256:75f7b3277acc7b6598495af7012661af4c5f51d956312902ce8ec2176e9bb06d"}, ] [package.dependencies] -botocore = "1.34.105" +botocore = "1.34.106" colorama = ">=0.2.5,<0.4.7" docutils = ">=0.10,<0.17" PyYAML = ">=3.10,<6.1" @@ -422,13 +422,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.105" +version = "1.34.106" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.105-py3-none-any.whl", hash = "sha256:a459d060b541beecb50681e6e8a39313cca981e146a59ba7c5229d62f631a016"}, - {file = "botocore-1.34.105.tar.gz", hash = "sha256:727d5d3e800ac8b705fca6e19b6fefa1e728a81d62a712df9bd32ed0117c740b"}, + {file = "botocore-1.34.106-py3-none-any.whl", hash = "sha256:4baf0e27c2dfc4f4d0dee7c217c716e0782f9b30e8e1fff983fce237d88f73ae"}, + {file = "botocore-1.34.106.tar.gz", hash = "sha256:921fa5202f88c3e58fdcb4b3acffd56d65b24bca47092ee4b27aa988556c0be6"}, ] [package.dependencies] @@ -4800,4 +4800,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "1462eb2579412b905ba0a19cbb2bc2020953963b28f9516e318a425a4a3aa3b2" +content-hash = "99901801a3c2a2dff8728de427066d131f3dfd59d642f1622b989cb2be2518de" diff --git a/pyproject.toml b/pyproject.toml index 1edf5516c..2b8d74011 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ alembic = "==1.13.1" amqp = "==5.2.0" beautifulsoup4 = "==4.12.3" boto3 = "^1.34.103" -botocore = "^1.34.105" +botocore = "^1.34.106" cachetools = "==5.3.3" celery = {version = "==5.4.0", extras = ["redis"]} certifi = ">=2022.12.7" From 046e266daefc5d7a0d2906bb6c9dfd71538ab712 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 13:29:48 +0000 Subject: [PATCH 12/38] Bump boto3 from 1.34.103 to 1.34.106 Bumps [boto3](https://github.com/boto/boto3) from 1.34.103 to 1.34.106. - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.34.103...1.34.106) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- poetry.lock | 10 +++++----- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index b7c783cad..5354ae9b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -403,17 +403,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.103" +version = "1.34.106" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.103-py3-none-any.whl", hash = "sha256:59b6499f1bb423dd99de6566a20d0a7cf1a5476824be3a792290fd86600e8365"}, - {file = "boto3-1.34.103.tar.gz", hash = "sha256:58d097241f3895c4a4c80c9e606689c6e06d77f55f9f53a4cc02dee7e03938b9"}, + {file = "boto3-1.34.106-py3-none-any.whl", hash = "sha256:d3be4e1dd5d546a001cd4da805816934cbde9d395316546e9411fec341ade5cf"}, + {file = "boto3-1.34.106.tar.gz", hash = "sha256:6165b8cf1c7e625628ab28b32f9027064c8f5e5fca1c38d7fc228cd22069a19f"}, ] [package.dependencies] -botocore = ">=1.34.103,<1.35.0" +botocore = ">=1.34.106,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -4800,4 +4800,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "99901801a3c2a2dff8728de427066d131f3dfd59d642f1622b989cb2be2518de" +content-hash = "ed8a34b31a8739d1cb852c90f2004efccc00458c06fc5760a078b4225386e4db" diff --git a/pyproject.toml b/pyproject.toml index 2b8d74011..e64316780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ python = "^3.12.2" alembic = "==1.13.1" amqp = "==5.2.0" beautifulsoup4 = "==4.12.3" -boto3 = "^1.34.103" +boto3 = "^1.34.106" botocore = "^1.34.106" cachetools = "==5.3.3" celery = {version = "==5.4.0", extras = ["redis"]} From 99edc88197b012b7f864d01cdc5006700b7098df Mon Sep 17 00:00:00 2001 From: Carlo Costino Date: Thu, 16 May 2024 10:17:45 -0400 Subject: [PATCH 13/38] Localize notification_utils to the API This changeset pulls in all of the notification_utils code directly into the API and removes it as an external dependency. We are doing this to cut down on operational maintenance of the project and will begin removing parts of it no longer needed for the API. Signed-off-by: Carlo Costino --- Makefile | 9 +- README.md | 17 - app/__init__.py | 8 +- app/authentication/auth.py | 2 +- app/celery/scheduled_tasks.py | 2 +- app/celery/tasks.py | 2 +- app/commands.py | 4 +- app/config.py | 2 +- app/dao/notifications_dao.py | 12 +- app/delivery/send_to_providers.py | 10 +- app/errors.py | 2 +- app/models.py | 18 +- app/notifications/process_notifications.py | 12 +- app/notifications/receive_notifications.py | 2 +- app/notifications/rest.py | 2 +- app/notifications/validators.py | 20 +- app/organization/invite_rest.py | 2 +- app/schema_validation/__init__.py | 1 + app/schemas.py | 12 +- app/serialised_models.py | 10 +- app/service/utils.py | 2 +- app/service_invite/rest.py | 2 +- app/template/rest.py | 4 +- app/user/rest.py | 2 +- app/utils.py | 3 +- app/v2/errors.py | 2 +- app/v2/notifications/post_notifications.py | 2 +- notifications_utils/__init__.py | 25 + notifications_utils/base64_uuid.py | 22 + notifications_utils/clients/__init__.py | 0 .../clients/antivirus/__init__.py | 0 .../clients/antivirus/antivirus_client.py | 55 + .../clients/encryption/__init__.py | 0 .../clients/encryption/encryption_client.py | 86 + notifications_utils/clients/redis/__init__.py | 13 + .../clients/redis/redis_client.py | 184 + .../clients/redis/request_cache.py | 95 + .../clients/zendesk/__init__.py | 0 .../clients/zendesk/zendesk_client.py | 150 + notifications_utils/countries/__init__.py | 81 + .../countries/_data/ended-countries.json | 6 + .../countries/_data/europe.txt | 62 + .../countries/_data/european-islands.txt | 5 + .../_data/location-autocomplete-graph.json | 28607 ++++++++++++++++ .../countries/_data/synonyms.json | 58 + .../countries/_data/uk-islands.txt | 8 + .../countries/_data/welsh-names.json | 103 + notifications_utils/countries/data.py | 67 + notifications_utils/field.py | 208 + notifications_utils/formatters.py | 349 + notifications_utils/insensitive_dict.py | 59 + .../international_billing_rates.py | 31 + .../international_billing_rates.yml | 2891 ++ .../broadcast_preview_template.jinja2 | 12 + .../email_preview_template.jinja2 | 39 + .../jinja_templates/email_template.jinja2 | 252 + .../letter_image_template.jinja2 | 37 + .../jinja_templates/letter_pdf/_body.jinja2 | 33 + .../jinja_templates/letter_pdf/_head.jinja2 | 7 + .../letter_pdf/_main_css.jinja2 | 197 + .../letter_pdf/_print_only_css.jinja2 | 17 + .../jinja_templates/letter_pdf/preview.jinja2 | 3 + .../jinja_templates/letter_pdf/print.jinja2 | 4 + .../sms_preview_template.jinja2 | 13 + notifications_utils/letter_timings.py | 180 + notifications_utils/logging.py | 133 + notifications_utils/markdown.py | 308 + notifications_utils/postal_address.py | 185 + notifications_utils/recipients.py | 743 + notifications_utils/request_helper.py | 123 + notifications_utils/s3.py | 87 + notifications_utils/safe_string.py | 25 + notifications_utils/sanitise_text.py | 310 + notifications_utils/serialised_model.py | 61 + notifications_utils/take.py | 3 + notifications_utils/template.py | 977 + notifications_utils/template_change.py | 31 + notifications_utils/timezones.py | 16 + notifications_utils/url_safe_token.py | 13 + poetry.lock | 237 +- pyproject.toml | 14 +- tests/app/celery/test_scheduled_tasks.py | 2 +- tests/app/celery/test_tasks.py | 4 +- .../test_process_notification.py | 8 +- tests/app/notifications/test_validators.py | 2 +- tests/app/organization/test_invite_rest.py | 2 +- .../test_send_notification.py | 2 +- .../test_send_one_off_notification.py | 4 +- .../test_service_invite_rest.py | 2 +- tests/app/template/test_rest.py | 2 +- tests/notification_utils/__init__.py | 0 .../antivirus/test_antivirus_client.py | 63 + .../encryption/test_encryption_client.py | 88 + .../clients/redis/test_redis_client.py | 221 + .../clients/redis/test_request_cache.py | 190 + .../notification_utils/clients/test_redis.py | 7 + .../clients/zendesk/test_zendesk_client.py | 220 + tests/notification_utils/conftest.py | 45 + tests/notification_utils/country_synonyms.py | 1937 ++ tests/notification_utils/test_base64_uuid.py | 57 + .../notification_utils/test_base_template.py | 119 + tests/notification_utils/test_countries.py | 170 + .../notification_utils/test_countries_iso.py | 526 + tests/notification_utils/test_field.py | 311 + .../test_field_html_handling.py | 71 + .../test_files/multi_page_pdf.pdf | Bin 0 -> 54836 bytes .../test_files/one_page_pdf.pdf | Bin 0 -> 91682 bytes .../notification_utils/test_formatted_list.py | 0 tests/notification_utils/test_formatters.py | 577 + .../test_insensitive_dict.py | 96 + .../test_international_billing_rates.py | 50 + .../notification_utils/test_letter_timings.py | 269 + tests/notification_utils/test_logging.py | 51 + tests/notification_utils/test_markdown.py | 667 + tests/notification_utils/test_placeholders.py | 66 + .../notification_utils/test_postal_address.py | 777 + .../notification_utils/test_recipient_csv.py | 1356 + .../test_recipient_validation.py | 428 + .../test_request_header_authentication.py | 61 + tests/notification_utils/test_request_id.py | 32 + tests/notification_utils/test_s3.py | 108 + tests/notification_utils/test_safe_string.py | 47 + .../notification_utils/test_sanitise_text.py | 313 + .../test_serialised_model.py | 220 + tests/notification_utils/test_take.py | 19 + .../test_template_change.py | 135 + .../notification_utils/test_template_types.py | 3388 ++ tests/notification_utils/test_timezones.py | 36 + .../test_url_safe_tokens.py | 36 + 129 files changed, 49913 insertions(+), 263 deletions(-) create mode 100644 notifications_utils/__init__.py create mode 100644 notifications_utils/base64_uuid.py create mode 100644 notifications_utils/clients/__init__.py create mode 100644 notifications_utils/clients/antivirus/__init__.py create mode 100644 notifications_utils/clients/antivirus/antivirus_client.py create mode 100644 notifications_utils/clients/encryption/__init__.py create mode 100644 notifications_utils/clients/encryption/encryption_client.py create mode 100644 notifications_utils/clients/redis/__init__.py create mode 100644 notifications_utils/clients/redis/redis_client.py create mode 100644 notifications_utils/clients/redis/request_cache.py create mode 100644 notifications_utils/clients/zendesk/__init__.py create mode 100644 notifications_utils/clients/zendesk/zendesk_client.py create mode 100644 notifications_utils/countries/__init__.py create mode 100644 notifications_utils/countries/_data/ended-countries.json create mode 100644 notifications_utils/countries/_data/europe.txt create mode 100644 notifications_utils/countries/_data/european-islands.txt create mode 100644 notifications_utils/countries/_data/location-autocomplete-graph.json create mode 100644 notifications_utils/countries/_data/synonyms.json create mode 100644 notifications_utils/countries/_data/uk-islands.txt create mode 100644 notifications_utils/countries/_data/welsh-names.json create mode 100644 notifications_utils/countries/data.py create mode 100644 notifications_utils/field.py create mode 100644 notifications_utils/formatters.py create mode 100644 notifications_utils/insensitive_dict.py create mode 100644 notifications_utils/international_billing_rates.py create mode 100644 notifications_utils/international_billing_rates.yml create mode 100644 notifications_utils/jinja_templates/broadcast_preview_template.jinja2 create mode 100644 notifications_utils/jinja_templates/email_preview_template.jinja2 create mode 100644 notifications_utils/jinja_templates/email_template.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_image_template.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/_body.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/_head.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/_main_css.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/_print_only_css.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/preview.jinja2 create mode 100644 notifications_utils/jinja_templates/letter_pdf/print.jinja2 create mode 100644 notifications_utils/jinja_templates/sms_preview_template.jinja2 create mode 100644 notifications_utils/letter_timings.py create mode 100644 notifications_utils/logging.py create mode 100644 notifications_utils/markdown.py create mode 100644 notifications_utils/postal_address.py create mode 100644 notifications_utils/recipients.py create mode 100644 notifications_utils/request_helper.py create mode 100644 notifications_utils/s3.py create mode 100644 notifications_utils/safe_string.py create mode 100644 notifications_utils/sanitise_text.py create mode 100644 notifications_utils/serialised_model.py create mode 100644 notifications_utils/take.py create mode 100644 notifications_utils/template.py create mode 100644 notifications_utils/template_change.py create mode 100644 notifications_utils/timezones.py create mode 100644 notifications_utils/url_safe_token.py create mode 100644 tests/notification_utils/__init__.py create mode 100644 tests/notification_utils/clients/antivirus/test_antivirus_client.py create mode 100644 tests/notification_utils/clients/encryption/test_encryption_client.py create mode 100644 tests/notification_utils/clients/redis/test_redis_client.py create mode 100644 tests/notification_utils/clients/redis/test_request_cache.py create mode 100644 tests/notification_utils/clients/test_redis.py create mode 100644 tests/notification_utils/clients/zendesk/test_zendesk_client.py create mode 100644 tests/notification_utils/conftest.py create mode 100644 tests/notification_utils/country_synonyms.py create mode 100644 tests/notification_utils/test_base64_uuid.py create mode 100644 tests/notification_utils/test_base_template.py create mode 100644 tests/notification_utils/test_countries.py create mode 100644 tests/notification_utils/test_countries_iso.py create mode 100644 tests/notification_utils/test_field.py create mode 100644 tests/notification_utils/test_field_html_handling.py create mode 100644 tests/notification_utils/test_files/multi_page_pdf.pdf create mode 100644 tests/notification_utils/test_files/one_page_pdf.pdf create mode 100644 tests/notification_utils/test_formatted_list.py create mode 100644 tests/notification_utils/test_formatters.py create mode 100644 tests/notification_utils/test_insensitive_dict.py create mode 100644 tests/notification_utils/test_international_billing_rates.py create mode 100644 tests/notification_utils/test_letter_timings.py create mode 100644 tests/notification_utils/test_logging.py create mode 100644 tests/notification_utils/test_markdown.py create mode 100644 tests/notification_utils/test_placeholders.py create mode 100644 tests/notification_utils/test_postal_address.py create mode 100644 tests/notification_utils/test_recipient_csv.py create mode 100644 tests/notification_utils/test_recipient_validation.py create mode 100644 tests/notification_utils/test_request_header_authentication.py create mode 100644 tests/notification_utils/test_request_id.py create mode 100644 tests/notification_utils/test_s3.py create mode 100644 tests/notification_utils/test_safe_string.py create mode 100644 tests/notification_utils/test_sanitise_text.py create mode 100644 tests/notification_utils/test_serialised_model.py create mode 100644 tests/notification_utils/test_take.py create mode 100644 tests/notification_utils/test_template_change.py create mode 100644 tests/notification_utils/test_template_types.py create mode 100644 tests/notification_utils/test_timezones.py create mode 100644 tests/notification_utils/test_url_safe_tokens.py diff --git a/Makefile b/Makefile index 0b94fd752..6574c6181 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ test: ## Run tests and create coverage report poetry run black . poetry run flake8 . poetry run isort --check-only ./app ./tests - poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10 + poetry run coverage run -m pytest --maxfail=10 poetry run coverage report -m --fail-under=95 poetry run coverage html -d .coverage_cache @@ -90,13 +90,6 @@ py-lock: ## Syncs dependencies and updates lock file without performing recursiv poetry lock --no-update poetry install --sync -.PHONY: update-utils -update-utils: ## Forces Poetry to pull the latest changes from the notifications-utils repo; requires that you commit the changes to poetry.lock! - poetry update notifications-utils - @echo - @echo !!! PLEASE MAKE SURE TO COMMIT AND PUSH THE UPDATED poetry.lock FILE !!! - @echo - .PHONY: freeze-requirements freeze-requirements: ## Pin all requirements including sub dependencies into requirements.txt poetry export --without-hashes --format=requirements.txt > requirements.txt diff --git a/README.md b/README.md index 161406485..0c4d14861 100644 --- a/README.md +++ b/README.md @@ -431,23 +431,6 @@ In either situation, once you are finished and have verified the dependency changes are working, please be sure to commit both the `pyproject.toml` and `poetry.lock` files. -### Keeping the notification-utils Dependency Up-to-Date - -The `notifications-utils` dependency references the other repository we have at -https://github.com/GSA/notifications-utils - this dependency requires a bit of -extra legwork to ensure it stays up-to-date. - -Whenever a PR is merged in the `notifications-utils` repository, we need to make -sure the changes are pulled in here and committed to this repository as well. -You can do this by going through these steps: - -- Make sure your local `main` branch is up-to-date -- Create a new branch to work in -- Run `make update-utils` -- Commit the updated `poetry.lock` file and push the changes -- Make a new PR with the change -- Have the PR get reviewed and merged - ## Known Installation Issues ### Python Installation Errors diff --git a/app/__init__.py b/app/__init__.py index f73625cb3..f9ce7a9ee 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -12,10 +12,6 @@ from flask.ctx import has_app_context from flask_marshmallow import Marshmallow from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy -from notifications_utils import logging, request_helper -from notifications_utils.clients.encryption.encryption_client import Encryption -from notifications_utils.clients.redis.redis_client import RedisClient -from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient from sqlalchemy import event from werkzeug.exceptions import HTTPException as WerkzeugHTTPException from werkzeug.local import LocalProxy @@ -26,6 +22,10 @@ from app.clients.document_download import DocumentDownloadClient from app.clients.email.aws_ses import AwsSesClient from app.clients.email.aws_ses_stub import AwsSesStubClient from app.clients.sms.aws_sns import AwsSnsClient +from notifications_utils import logging, request_helper +from notifications_utils.clients.encryption.encryption_client import Encryption +from notifications_utils.clients.redis.redis_client import RedisClient +from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient class NotifyCelery(Celery): diff --git a/app/authentication/auth.py b/app/authentication/auth.py index b3b4981a1..a85cd2b4f 100644 --- a/app/authentication/auth.py +++ b/app/authentication/auth.py @@ -12,10 +12,10 @@ from notifications_python_client.errors import ( TokenExpiredError, TokenIssuerError, ) -from notifications_utils import request_helper from sqlalchemy.orm.exc import NoResultFound from app.serialised_models import SerialisedService +from notifications_utils import request_helper # stvnrlly - this is silly, but bandit has a multiline string bug (https://github.com/PyCQA/bandit/issues/658) # and flake8 wants a multiline quote here. TODO: check on bug status and restore sanity once possible diff --git a/app/celery/scheduled_tasks.py b/app/celery/scheduled_tasks.py index 2c4d31d8c..7da92a2a3 100644 --- a/app/celery/scheduled_tasks.py +++ b/app/celery/scheduled_tasks.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta from flask import current_app -from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket from sqlalchemy import between from sqlalchemy.exc import SQLAlchemyError @@ -32,6 +31,7 @@ from app.dao.users_dao import delete_codes_older_created_more_than_a_day_ago from app.enums import JobStatus, NotificationType from app.models import Job from app.notifications.process_notifications import send_notification_to_queue +from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket MAX_NOTIFICATION_FAILS = 10000 diff --git a/app/celery/tasks.py b/app/celery/tasks.py index 9ab84cb2e..c94b93789 100644 --- a/app/celery/tasks.py +++ b/app/celery/tasks.py @@ -2,7 +2,6 @@ import json from datetime import datetime from flask import current_app -from notifications_utils.recipients import RecipientCSV from requests import HTTPError, RequestException, request from sqlalchemy.exc import IntegrityError, SQLAlchemyError @@ -27,6 +26,7 @@ from app.serialised_models import SerialisedService, SerialisedTemplate from app.service.utils import service_allowed_to_send_to from app.utils import DATETIME_FORMAT from app.v2.errors import TotalRequestsError +from notifications_utils.recipients import RecipientCSV @notify_celery.task(name="process-job") diff --git a/app/commands.py b/app/commands.py index 5d2fd9231..5637832cb 100644 --- a/app/commands.py +++ b/app/commands.py @@ -12,8 +12,6 @@ from click_datetime import Datetime as click_dt from faker import Faker from flask import current_app, json from notifications_python_client.authentication import create_jwt_token -from notifications_utils.recipients import RecipientCSV -from notifications_utils.template import SMSMessageTemplate from sqlalchemy import and_, text from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound @@ -64,6 +62,8 @@ from app.models import ( User, ) from app.utils import get_midnight_in_utc +from notifications_utils.recipients import RecipientCSV +from notifications_utils.template import SMSMessageTemplate from tests.app.db import ( create_job, create_notification, diff --git a/app/config.py b/app/config.py index 809a71ebe..8d913bdd8 100644 --- a/app/config.py +++ b/app/config.py @@ -2,10 +2,10 @@ import json from datetime import timedelta from os import getenv, path -import notifications_utils from celery.schedules import crontab from kombu import Exchange, Queue +import notifications_utils from app.cloudfoundry_config import cloud_config diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 2928046b6..f00ae4a9b 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -1,12 +1,6 @@ from datetime import datetime, timedelta from flask import current_app -from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES -from notifications_utils.recipients import ( - InvalidEmailError, - try_validate_and_format_phone_number, - validate_and_format_email_address, -) from sqlalchemy import asc, desc, or_, select, text, union from sqlalchemy.orm import joinedload from sqlalchemy.orm.exc import NoResultFound @@ -23,6 +17,12 @@ from app.utils import ( get_midnight_in_utc, midnight_n_days_ago, ) +from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES +from notifications_utils.recipients import ( + InvalidEmailError, + try_validate_and_format_phone_number, + validate_and_format_email_address, +) def dao_get_last_date_template_was_used(template_id, service_id): diff --git a/app/delivery/send_to_providers.py b/app/delivery/send_to_providers.py index 98f86396a..8a06d820a 100644 --- a/app/delivery/send_to_providers.py +++ b/app/delivery/send_to_providers.py @@ -4,11 +4,6 @@ from urllib import parse from cachetools import TTLCache, cached from flask import current_app -from notifications_utils.template import ( - HTMLEmailTemplate, - PlainTextEmailTemplate, - SMSMessageTemplate, -) from app import create_uuid, db, notification_provider_clients, redis_store from app.aws.s3 import get_personalisation_from_s3, get_phone_number_from_s3 @@ -19,6 +14,11 @@ from app.dao.provider_details_dao import get_provider_details_by_notification_ty from app.enums import BrandType, KeyType, NotificationStatus, NotificationType from app.exceptions import NotificationTechnicalFailureException from app.serialised_models import SerialisedService, SerialisedTemplate +from notifications_utils.template import ( + HTMLEmailTemplate, + PlainTextEmailTemplate, + SMSMessageTemplate, +) def send_sms_to_provider(notification): diff --git a/app/errors.py b/app/errors.py index f01fc3857..1278d3253 100644 --- a/app/errors.py +++ b/app/errors.py @@ -1,12 +1,12 @@ from flask import current_app, json, jsonify from jsonschema import ValidationError as JsonSchemaValidationError from marshmallow import ValidationError -from notifications_utils.recipients import InvalidEmailError from sqlalchemy.exc import DataError from sqlalchemy.orm.exc import NoResultFound from app.authentication.auth import AuthError from app.exceptions import ArchiveValidationError +from notifications_utils.recipients import InvalidEmailError class VirusScanError(Exception): diff --git a/app/models.py b/app/models.py index dd954e157..71eea3295 100644 --- a/app/models.py +++ b/app/models.py @@ -3,15 +3,6 @@ import itertools import uuid from flask import current_app, url_for -from notifications_utils.clients.encryption.encryption_client import EncryptionError -from notifications_utils.recipients import ( - InvalidEmailError, - InvalidPhoneError, - try_validate_and_format_phone_number, - validate_email_address, - validate_phone_number, -) -from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate from sqlalchemy import CheckConstraint, Index, UniqueConstraint from sqlalchemy.dialects.postgresql import JSON, JSONB, UUID from sqlalchemy.ext.associationproxy import association_proxy @@ -46,6 +37,15 @@ from app.utils import ( DATETIME_FORMAT_NO_TIMEZONE, get_dt_string_or_none, ) +from notifications_utils.clients.encryption.encryption_client import EncryptionError +from notifications_utils.recipients import ( + InvalidEmailError, + InvalidPhoneError, + try_validate_and_format_phone_number, + validate_email_address, + validate_phone_number, +) +from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate def filter_null_value_fields(obj): diff --git a/app/notifications/process_notifications.py b/app/notifications/process_notifications.py index 0f0d33d6b..8f542d31a 100644 --- a/app/notifications/process_notifications.py +++ b/app/notifications/process_notifications.py @@ -2,12 +2,6 @@ import uuid from datetime import datetime from flask import current_app -from notifications_utils.recipients import ( - format_email_address, - get_international_phone_info, - validate_and_format_phone_number, -) -from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate from app import redis_store from app.celery import provider_tasks @@ -19,6 +13,12 @@ from app.dao.notifications_dao import ( from app.enums import KeyType, NotificationStatus, NotificationType from app.models import Notification from app.v2.errors import BadRequestError +from notifications_utils.recipients import ( + format_email_address, + get_international_phone_info, + validate_and_format_phone_number, +) +from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate def create_content_for_notification(template, personalisation): diff --git a/app/notifications/receive_notifications.py b/app/notifications/receive_notifications.py index 820d66cb4..ac25ae3ae 100644 --- a/app/notifications/receive_notifications.py +++ b/app/notifications/receive_notifications.py @@ -1,5 +1,4 @@ from flask import Blueprint, current_app, json, jsonify, request -from notifications_utils.recipients import try_validate_and_format_phone_number from app.celery import tasks from app.config import QueueNames @@ -9,6 +8,7 @@ from app.enums import ServicePermissionType from app.errors import InvalidRequest, register_errors from app.models import InboundSms from app.notifications.sns_handlers import sns_notification_handler +from notifications_utils.recipients import try_validate_and_format_phone_number receive_notifications_blueprint = Blueprint("receive_notifications", __name__) register_errors(receive_notifications_blueprint) diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 55d3c101a..9c5806ede 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -1,5 +1,4 @@ from flask import Blueprint, current_app, jsonify, request -from notifications_utils import SMS_CHAR_COUNT_LIMIT from app import api_user, authenticated_service from app.aws.s3 import get_personalisation_from_s3, get_phone_number_from_s3 @@ -26,6 +25,7 @@ from app.schemas import ( ) from app.service.utils import service_allowed_to_send_to from app.utils import get_public_notify_type_text, pagination_links +from notifications_utils import SMS_CHAR_COUNT_LIMIT notifications = Blueprint("notifications", __name__) diff --git a/app/notifications/validators.py b/app/notifications/validators.py index 94fca2e02..25cc7eb66 100644 --- a/app/notifications/validators.py +++ b/app/notifications/validators.py @@ -1,14 +1,4 @@ from flask import current_app -from notifications_utils import SMS_CHAR_COUNT_LIMIT -from notifications_utils.clients.redis import ( - rate_limit_cache_key, - total_limit_cache_key, -) -from notifications_utils.recipients import ( - get_international_phone_info, - validate_and_format_email_address, - validate_and_format_phone_number, -) from sqlalchemy.orm.exc import NoResultFound from app import redis_store @@ -22,6 +12,16 @@ from app.serialised_models import SerialisedTemplate from app.service.utils import service_allowed_to_send_to from app.utils import get_public_notify_type_text from app.v2.errors import BadRequestError, RateLimitError, TotalRequestsError +from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.clients.redis import ( + rate_limit_cache_key, + total_limit_cache_key, +) +from notifications_utils.recipients import ( + get_international_phone_info, + validate_and_format_email_address, + validate_and_format_phone_number, +) def check_service_over_api_rate_limit(service, api_key): diff --git a/app/organization/invite_rest.py b/app/organization/invite_rest.py index 9889bb157..f87605435 100644 --- a/app/organization/invite_rest.py +++ b/app/organization/invite_rest.py @@ -3,7 +3,6 @@ import os from flask import Blueprint, current_app, jsonify, request from itsdangerous import BadData, SignatureExpired -from notifications_utils.url_safe_token import check_token, generate_token from app import redis_store from app.config import QueueNames @@ -28,6 +27,7 @@ from app.organization.organization_schema import ( post_update_invited_org_user_status_schema, ) from app.schema_validation import validate +from notifications_utils.url_safe_token import check_token, generate_token organization_invite_blueprint = Blueprint("organization_invite", __name__) diff --git a/app/schema_validation/__init__.py b/app/schema_validation/__init__.py index 40d98dce1..c4f8f6486 100644 --- a/app/schema_validation/__init__.py +++ b/app/schema_validation/__init__.py @@ -4,6 +4,7 @@ from uuid import UUID from iso8601 import ParseError, iso8601 from jsonschema import Draft7Validator, FormatChecker, ValidationError + from notifications_utils.recipients import ( InvalidEmailError, InvalidPhoneError, diff --git a/app/schemas.py b/app/schemas.py index b975e3c2d..7b47da593 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -14,6 +14,12 @@ from marshmallow import ( validates_schema, ) from marshmallow_sqlalchemy import auto_field, field_for + +from app import ma, models +from app.dao.permissions_dao import permission_dao +from app.enums import ServicePermissionType, TemplateType +from app.models import ServicePermission +from app.utils import DATETIME_FORMAT_NO_TIMEZONE, get_template_instance from notifications_utils.recipients import ( InvalidEmailError, InvalidPhoneError, @@ -22,12 +28,6 @@ from notifications_utils.recipients import ( validate_phone_number, ) -from app import ma, models -from app.dao.permissions_dao import permission_dao -from app.enums import ServicePermissionType, TemplateType -from app.models import ServicePermission -from app.utils import DATETIME_FORMAT_NO_TIMEZONE, get_template_instance - def _validate_positive_number(value, msg="Not a positive integer"): try: diff --git a/app/serialised_models.py b/app/serialised_models.py index d9a227ccd..d19e2fd27 100644 --- a/app/serialised_models.py +++ b/app/serialised_models.py @@ -4,16 +4,16 @@ from threading import RLock import cachetools from flask import current_app -from notifications_utils.clients.redis import RequestCache -from notifications_utils.serialised_model import ( - SerialisedModel, - SerialisedModelCollection, -) from werkzeug.utils import cached_property from app import db, redis_store from app.dao.api_key_dao import get_model_api_keys from app.dao.services_dao import dao_fetch_service_by_id +from notifications_utils.clients.redis import RequestCache +from notifications_utils.serialised_model import ( + SerialisedModel, + SerialisedModelCollection, +) caches = defaultdict(partial(cachetools.TTLCache, maxsize=1024, ttl=2)) locks = defaultdict(RLock) diff --git a/app/service/utils.py b/app/service/utils.py index ac0613096..b8fe6ba8f 100644 --- a/app/service/utils.py +++ b/app/service/utils.py @@ -1,11 +1,11 @@ import itertools from flask import current_app -from notifications_utils.recipients import allowed_to_send_to from app.dao.services_dao import dao_fetch_service_by_id from app.enums import KeyType, RecipientType from app.models import ServiceGuestList +from notifications_utils.recipients import allowed_to_send_to def get_recipients_from_request(request_json, key, type): diff --git a/app/service_invite/rest.py b/app/service_invite/rest.py index 629a01d47..02899d3e9 100644 --- a/app/service_invite/rest.py +++ b/app/service_invite/rest.py @@ -5,7 +5,6 @@ from datetime import datetime from flask import Blueprint, current_app, jsonify, request from itsdangerous import BadData, SignatureExpired -from notifications_utils.url_safe_token import check_token, generate_token from app import redis_store from app.config import QueueNames @@ -27,6 +26,7 @@ from app.notifications.process_notifications import ( ) from app.schemas import invited_user_schema from app.utils import hilite +from notifications_utils.url_safe_token import check_token, generate_token service_invite = Blueprint("service_invite", __name__) diff --git a/app/template/rest.py b/app/template/rest.py index 750b93891..dee51fd05 100644 --- a/app/template/rest.py +++ b/app/template/rest.py @@ -1,6 +1,4 @@ from flask import Blueprint, jsonify, request -from notifications_utils import SMS_CHAR_COUNT_LIMIT -from notifications_utils.template import SMSMessageTemplate from sqlalchemy.orm.exc import NoResultFound from app.dao.services_dao import dao_fetch_service_by_id @@ -28,6 +26,8 @@ from app.template.template_schemas import ( post_update_template_schema, ) from app.utils import get_public_notify_type_text +from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.template import SMSMessageTemplate template_blueprint = Blueprint( "template", __name__, url_prefix="/service//template" diff --git a/app/user/rest.py b/app/user/rest.py index 88236216b..ea2da8eee 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -4,7 +4,6 @@ from datetime import datetime from urllib.parse import urlencode from flask import Blueprint, abort, current_app, jsonify, request -from notifications_utils.recipients import is_us_phone_number, use_numeric_sender from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound @@ -57,6 +56,7 @@ from app.user.users_schema import ( post_verify_webauthn_schema, ) from app.utils import url_with_token +from notifications_utils.recipients import is_us_phone_number, use_numeric_sender user_blueprint = Blueprint("user", __name__) register_errors(user_blueprint) diff --git a/app/utils.py b/app/utils.py index 9519e2076..22f9a034c 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,9 +1,10 @@ from datetime import datetime, timedelta from flask import url_for -from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate from sqlalchemy import func +from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate + DATETIME_FORMAT_NO_TIMEZONE = "%Y-%m-%d %H:%M:%S.%f" DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" DATE_FORMAT = "%Y-%m-%d" diff --git a/app/v2/errors.py b/app/v2/errors.py index ccb353428..0888e2073 100644 --- a/app/v2/errors.py +++ b/app/v2/errors.py @@ -2,13 +2,13 @@ import json from flask import current_app, jsonify, request from jsonschema import ValidationError as JsonSchemaValidationError -from notifications_utils.recipients import InvalidEmailError from sqlalchemy.exc import DataError from sqlalchemy.orm.exc import NoResultFound from app.authentication.auth import AuthError from app.enums import KeyType from app.errors import InvalidRequest +from notifications_utils.recipients import InvalidEmailError class TooManyRequestsError(InvalidRequest): diff --git a/app/v2/notifications/post_notifications.py b/app/v2/notifications/post_notifications.py index 210f32c6d..3c8fa1fdb 100644 --- a/app/v2/notifications/post_notifications.py +++ b/app/v2/notifications/post_notifications.py @@ -4,7 +4,6 @@ from datetime import datetime import botocore from flask import abort, current_app, jsonify, request -from notifications_utils.recipients import try_validate_and_format_phone_number from app import api_user, authenticated_service, document_download_client, encryption from app.celery.tasks import save_api_email, save_api_sms @@ -40,6 +39,7 @@ from app.v2.notifications.notification_schemas import ( post_sms_request, ) from app.v2.utils import get_valid_json +from notifications_utils.recipients import try_validate_and_format_phone_number @v2_notification_blueprint.route("/", methods=["POST"]) diff --git a/notifications_utils/__init__.py b/notifications_utils/__init__.py new file mode 100644 index 000000000..84a55d644 --- /dev/null +++ b/notifications_utils/__init__.py @@ -0,0 +1,25 @@ +import re + +SMS_CHAR_COUNT_LIMIT = 918 # 153 * 6, no network issues but check with providers before upping this further +LETTER_MAX_PAGE_COUNT = 10 +DAILY_MESSAGE_LIMIT = 10000 + +# regexes for use in recipients.validate_email_address. +# Valid characters taken from https://en.wikipedia.org/wiki/Email_address#Local-part +# Note: Normal apostrophe eg `Firstname-o'surname@domain.com` is allowed. +# hostname_part regex: xn in regex signifies possible punycode conversions, which would start `xn--`; +# the hyphens are matched for later in the regex. +hostname_part = re.compile(r"^(xn|[a-z0-9]+)(-?-[a-z0-9]+)*$", re.IGNORECASE) +tld_part = re.compile(r"^([a-z]{2,63}|xn--([a-z0-9]+-)*[a-z0-9]+)$", re.IGNORECASE) +VALID_LOCAL_CHARS = r"a-zA-Z0-9.!#$%&'*+/=?^_`{|}~\-" +EMAIL_REGEX_PATTERN = r"^[{}]+@([^.@][^@\s]+)$".format(VALID_LOCAL_CHARS) +email_with_smart_quotes_regex = re.compile( + # matches wider than an email - everything between an at sign and the nearest whitespace + r"(^|\s)\S+@\S+(\s|$)", + flags=re.MULTILINE, +) + +# The magic sequence is a ‘unique’ series of characters which we temporarily insert +# and then later remove when performing tricky formatting operations +MAGIC_SEQUENCE = "đŸ‡Ŧ🇧đŸĻâœ‰ī¸" +magic_sequence_regex = re.compile(MAGIC_SEQUENCE) diff --git a/notifications_utils/base64_uuid.py b/notifications_utils/base64_uuid.py new file mode 100644 index 000000000..9721bf2ec --- /dev/null +++ b/notifications_utils/base64_uuid.py @@ -0,0 +1,22 @@ +from base64 import urlsafe_b64decode, urlsafe_b64encode +from uuid import UUID + + +def base64_to_bytes(key): + return urlsafe_b64decode(key + "==") + + +def bytes_to_base64(bytes): + # remove trailing = to save precious bytes + return urlsafe_b64encode(bytes).decode("ascii").rstrip("=") + + +def base64_to_uuid(value): + # uuids are 16 bytes, and will always have two ==s of padding + return UUID(bytes=urlsafe_b64decode(value.encode("ascii") + b"==")) + + +def uuid_to_base64(value): + if not isinstance(value, UUID): + value = UUID(value) + return bytes_to_base64(value.bytes) diff --git a/notifications_utils/clients/__init__.py b/notifications_utils/clients/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications_utils/clients/antivirus/__init__.py b/notifications_utils/clients/antivirus/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications_utils/clients/antivirus/antivirus_client.py b/notifications_utils/clients/antivirus/antivirus_client.py new file mode 100644 index 000000000..affe8f27e --- /dev/null +++ b/notifications_utils/clients/antivirus/antivirus_client.py @@ -0,0 +1,55 @@ +import requests +from flask import current_app + + +class AntivirusError(Exception): + def __init__(self, message=None, status_code=None): + self.message = message + self.status_code = status_code + + @classmethod + def from_exception(cls, e): + try: + message = e.response.json()["error"] + status_code = e.response.status_code + except (TypeError, ValueError, AttributeError, KeyError): + message = "connection error" + status_code = 503 + + return cls(message, status_code) + + +class AntivirusClient: + def __init__(self, api_host=None, auth_token=None): + self.api_host = api_host + self.auth_token = auth_token + + def init_app(self, app): + self.api_host = app.config["ANTIVIRUS_API_HOST"] + self.auth_token = app.config["ANTIVIRUS_API_KEY"] + + def scan(self, document_stream): + try: + response = requests.post( + "{}/scan".format(self.api_host), + headers={ + "Authorization": "Bearer {}".format(self.auth_token), + }, + files={"document": document_stream}, + ) + + response.raise_for_status() + + except requests.RequestException as e: + error = AntivirusError.from_exception(e) + current_app.logger.warning( + "Notify Antivirus API request failed with error: {}".format( + error.message + ) + ) + + raise error + finally: + document_stream.seek(0) + + return response.json()["ok"] diff --git a/notifications_utils/clients/encryption/__init__.py b/notifications_utils/clients/encryption/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications_utils/clients/encryption/encryption_client.py b/notifications_utils/clients/encryption/encryption_client.py new file mode 100644 index 000000000..cf5283208 --- /dev/null +++ b/notifications_utils/clients/encryption/encryption_client.py @@ -0,0 +1,86 @@ +from base64 import urlsafe_b64encode +from json import dumps, loads + +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from itsdangerous import BadSignature, URLSafeSerializer + + +class EncryptionError(Exception): + pass + + +class SaltLengthError(Exception): + pass + + +class Encryption: + def init_app(self, app): + self._serializer = URLSafeSerializer(app.config.get("SECRET_KEY")) + self._salt = app.config.get("DANGEROUS_SALT") + self._password = app.config.get("SECRET_KEY").encode() + + try: + self._shared_encryptor = Fernet(self._derive_key(self._salt)) + except SaltLengthError as reason: + raise EncryptionError( + "DANGEROUS_SALT must be at least 16 bytes" + ) from reason + + def encrypt(self, thing_to_encrypt, salt=None): + """Encrypt a string or object + + thing_to_encrypt must be serializable as JSON + Returns a UTF-8 string + """ + serialized_bytes = dumps(thing_to_encrypt).encode("utf-8") + encrypted_bytes = self._encryptor(salt).encrypt(serialized_bytes) + return encrypted_bytes.decode("utf-8") + + def decrypt(self, thing_to_decrypt, salt=None): + """Decrypt a UTF-8 string or bytes. + + Once decrypted, thing_to_decrypt must be deserializable from JSON. + """ + try: + return loads(self._encryptor(salt).decrypt(thing_to_decrypt)) + except InvalidToken as reason: + raise EncryptionError from reason + + def sign(self, thing_to_sign, salt=None): + return self._serializer.dumps(thing_to_sign, salt=(salt or self._salt)) + + def verify_signature(self, thing_to_verify, salt=None): + try: + return self._serializer.loads(thing_to_verify, salt=(salt or self._salt)) + except BadSignature as reason: + raise EncryptionError from reason + + def _encryptor(self, salt=None): + if salt is None: + return self._shared_encryptor + else: + try: + return Fernet(self._derive_key(salt)) + except SaltLengthError as reason: + raise EncryptionError( + "Custom salt value must be at least 16 bytes" + ) from reason + + def _derive_key(self, salt): + """Derive a key suitable for use within Fernet from the SECRET_KEY and salt + + * For the salt to be secure, it must be 16 bytes or longer and randomly generated. + * 600_000 was chosen for the iterations because it is what OWASP recommends as + * of [February 2023](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) + * For more information, see https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#pbkdf2 + * and https://cryptography.io/en/latest/fernet/#using-passwords-with-fernet + """ + salt_bytes = salt.encode() + if len(salt_bytes) < 16: + raise SaltLengthError + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), length=32, salt=salt_bytes, iterations=600_000 + ) + return urlsafe_b64encode(kdf.derive(self._password)) diff --git a/notifications_utils/clients/redis/__init__.py b/notifications_utils/clients/redis/__init__.py new file mode 100644 index 000000000..93a77d561 --- /dev/null +++ b/notifications_utils/clients/redis/__init__.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from .request_cache import RequestCache # noqa: F401 (unused import) + + +def total_limit_cache_key(service_id): + return "{}-{}-{}".format( + str(service_id), datetime.utcnow().strftime("%Y-%m-%d"), "total-count" + ) + + +def rate_limit_cache_key(service_id, api_key_type): + return "{}-{}".format(str(service_id), api_key_type) diff --git a/notifications_utils/clients/redis/redis_client.py b/notifications_utils/clients/redis/redis_client.py new file mode 100644 index 000000000..6e81d85bf --- /dev/null +++ b/notifications_utils/clients/redis/redis_client.py @@ -0,0 +1,184 @@ +import numbers +import uuid +from time import time + +from flask import current_app +from flask_redis import FlaskRedis + + +def prepare_value(val): + """ + Only bytes, strings and numbers (ints, longs and floats) are acceptable + for keys and values. Previously redis-py attempted to cast other types + to str() and store the result. This caused must confusion and frustration + when passing boolean values (cast to 'True' and 'False') or None values + (cast to 'None'). It is now the user's responsibility to cast all + key names and values to bytes, strings or numbers before passing the + value to redis-py. + """ + # things redis-py natively supports + if isinstance( + val, + ( + bytes, + str, + numbers.Number, + ), + ): + return val + # things we know we can safely cast to string + elif isinstance(val, (uuid.UUID,)): + return str(val) + else: + raise ValueError("cannot cast {} to a string".format(type(val))) + + +class RedisClient: + redis_store = FlaskRedis() + active = False + scripts = {} + + def init_app(self, app): + self.active = app.config.get("REDIS_ENABLED") + if self.active: + self.redis_store.init_app(app) + + self.register_scripts() + + def register_scripts(self): + # delete keys matching a pattern supplied as a parameter. Does so in batches of 5000 to prevent unpack from + # exceeding lua's stack limit, and also to prevent errors if no keys match the pattern. + # Inspired by https://gist.github.com/ddre54/0a4751676272e0da8186 + self.scripts["delete-keys-by-pattern"] = self.redis_store.register_script( + """ + local keys = redis.call('keys', ARGV[1]) + local deleted = 0 + for i=1, #keys, 5000 do + deleted = deleted + redis.call('del', unpack(keys, i, math.min(i + 4999, #keys))) + end + return deleted + """ + ) + + def delete_by_pattern(self, pattern, raise_exception=False): + r""" + Deletes all keys matching a given pattern, and returns how many keys were deleted. + Pattern is defined as in the KEYS command: https://redis.io/commands/keys + + * h?llo matches hello, hallo and hxllo + * h*llo matches hllo and heeeello + * h[ae]llo matches hello and hallo, but not hillo + * h[^e]llo matches hallo, hbllo, ... but not hello + * h[a-b]llo matches hallo and hbllo + + Use \ to escape special characters if you want to match them verbatim + """ + if self.active: + try: + return self.scripts["delete-keys-by-pattern"](args=[pattern]) + except Exception as e: + self.__handle_exception( + e, raise_exception, "delete-by-pattern", pattern + ) + + return 0 + + def exceeded_rate_limit(self, cache_key, limit, interval, raise_exception=False): + """ + Rate limiting. + - Uses Redis sorted sets + - Also uses redis "multi" which is abstracted into pipeline() by FlaskRedis/PyRedis + - Sends all commands to redis as a group to be executed atomically + + Method: + (1) Add event, scored by timestamp (zadd). The score determines order in set. + (2) Use zremrangebyscore to delete all set members with a score between + - Earliest entry (lowest score == earliest timestamp) - represented as '-inf' + and + - Current timestamp minus the interval + - Leaves only relevant entries in the set (those between now and now - interval) + (3) Count the set + (4) If count > limit fail request + (5) Ensure we expire the set key to preserve space + + Notes: + - Failed requests count. If over the limit and keep making requests you'll stay over the limit. + - The actual value in the set is just the timestamp, the same as the score. We don't store any requets details. + - return value of pipe.execute() is an array containing the outcome of each call. + - result[2] == outcome of pipe.zcard() + - If redis is inactive, or we get an exception, allow the request + + :param cache_key: + :param limit: Number of requests permitted within interval + :param interval: Interval we measure requests in + :param raise_exception: Should throw exception + :return: + """ + cache_key = prepare_value(cache_key) + if self.active: + try: + pipe = self.redis_store.pipeline() + when = time() + pipe.zadd(cache_key, {when: when}) + pipe.zremrangebyscore(cache_key, "-inf", when - interval) + pipe.zcard(cache_key) + pipe.expire(cache_key, interval) + result = pipe.execute() + return result[2] > limit + except Exception as e: + self.__handle_exception( + e, raise_exception, "rate-limit-pipeline", cache_key + ) + return False + else: + return False + + def raw_set(self, key, value, ex=None, px=None, nx=False, xx=False): + self.redis_store.set(key, value, ex, px, nx, xx) + + def set( + self, key, value, ex=None, px=None, nx=False, xx=False, raise_exception=False + ): + key = prepare_value(key) + value = prepare_value(value) + if self.active: + try: + self.redis_store.set(key, value, ex, px, nx, xx) + except Exception as e: + self.__handle_exception(e, raise_exception, "set", key) + + def incr(self, key, raise_exception=False): + key = prepare_value(key) + if self.active: + try: + return self.redis_store.incr(key) + except Exception as e: + self.__handle_exception(e, raise_exception, "incr", key) + + def raw_get(self, key): + return self.redis_store.get(key) + + def get(self, key, raise_exception=False): + key = prepare_value(key) + if self.active: + try: + return self.redis_store.get(key) + except Exception as e: + self.__handle_exception(e, raise_exception, "get", key) + + return None + + def delete(self, *keys, raise_exception=False): + keys = [prepare_value(k) for k in keys] + if self.active: + try: + self.redis_store.delete(*keys) + except Exception as e: + self.__handle_exception(e, raise_exception, "delete", ", ".join(keys)) + + def __handle_exception(self, e, raise_exception, operation, key_name): + current_app.logger.exception( + "Redis error performing {} on {}".format(operation, key_name) + ) + if raise_exception: + raise e diff --git a/notifications_utils/clients/redis/request_cache.py b/notifications_utils/clients/redis/request_cache.py new file mode 100644 index 000000000..edb45c98e --- /dev/null +++ b/notifications_utils/clients/redis/request_cache.py @@ -0,0 +1,95 @@ +import json +from contextlib import suppress +from datetime import timedelta +from functools import wraps +from inspect import signature + + +class RequestCache: + DEFAULT_TTL = int(timedelta(days=7).total_seconds()) + + def __init__(self, redis_client): + self.redis_client = redis_client + + @staticmethod + def _get_argument(argument_name, client_method, args, kwargs): + with suppress(KeyError): + return kwargs[argument_name] + + with suppress(ValueError, IndexError): + argument_index = list(signature(client_method).parameters).index( + argument_name + ) + return args[argument_index] + + with suppress(KeyError): + return signature(client_method).parameters[argument_name].default + + raise TypeError( + "{}() takes no argument called '{}'".format( + client_method.__name__, argument_name + ) + ) + + @staticmethod + def _make_key(key_format, client_method, args, kwargs): + return key_format.format( + **{ + argument_name: RequestCache._get_argument( + argument_name, client_method, args, kwargs + ) + for argument_name in list(signature(client_method).parameters) + } + ) + + def set(self, key_format, *, ttl_in_seconds=DEFAULT_TTL): + def _set(client_method): + @wraps(client_method) + def new_client_method(*args, **kwargs): + redis_key = RequestCache._make_key( + key_format, client_method, args, kwargs + ) + cached = self.redis_client.get(redis_key) + if cached: + return json.loads(cached.decode("utf-8")) + api_response = client_method(*args, **kwargs) + self.redis_client.set( + redis_key, + json.dumps(api_response), + ex=int(ttl_in_seconds), + ) + return api_response + + return new_client_method + + return _set + + def delete(self, key_format): + def _delete(client_method): + @wraps(client_method) + def new_client_method(*args, **kwargs): + try: + api_response = client_method(*args, **kwargs) + finally: + redis_key = self._make_key(key_format, client_method, args, kwargs) + self.redis_client.delete(redis_key) + return api_response + + return new_client_method + + return _delete + + def delete_by_pattern(self, key_format): + def _delete(client_method): + @wraps(client_method) + def new_client_method(*args, **kwargs): + try: + api_response = client_method(*args, **kwargs) + finally: + redis_key = self._make_key(key_format, client_method, args, kwargs) + self.redis_client.delete_by_pattern(redis_key) + return api_response + + return new_client_method + + return _delete diff --git a/notifications_utils/clients/zendesk/__init__.py b/notifications_utils/clients/zendesk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notifications_utils/clients/zendesk/zendesk_client.py b/notifications_utils/clients/zendesk/zendesk_client.py new file mode 100644 index 000000000..c5c2c5d02 --- /dev/null +++ b/notifications_utils/clients/zendesk/zendesk_client.py @@ -0,0 +1,150 @@ +import requests +from flask import current_app + + +class ZendeskError(Exception): + def __init__(self, response): + self.response = response + + +class ZendeskClient: + # the account used to authenticate with. If no requester is provided, the ticket will come from this account. + NOTIFY_ZENDESK_EMAIL = "zd-api-notify@digital.cabinet-office.gov.uk" + + ZENDESK_TICKET_URL = "https://govuk.zendesk.com/api/v2/tickets.json" + + def __init__(self): + self.api_key = None + + def init_app(self, app, *args, **kwargs): + self.api_key = app.config.get("ZENDESK_API_KEY") + + def send_ticket_to_zendesk(self, ticket): + response = requests.post( + self.ZENDESK_TICKET_URL, + json=ticket.request_data, + auth=(f"{self.NOTIFY_ZENDESK_EMAIL}/token", self.api_key), + ) + + if response.status_code != 201: + current_app.logger.error( + f"Zendesk create ticket request failed with {response.status_code} '{response.json()}'" + ) + raise ZendeskError(response) + + ticket_id = response.json()["ticket"]["id"] + + current_app.logger.info(f"Zendesk create ticket {ticket_id} succeeded") + + +class NotifySupportTicket: + PRIORITY_URGENT = "urgent" + PRIORITY_HIGH = "high" + PRIORITY_NORMAL = "normal" + PRIORITY_LOW = "low" + + TAGS_P2 = "govuk_notify_support" + TAGS_P1 = "govuk_notify_emergency" + + TYPE_PROBLEM = "problem" + TYPE_INCIDENT = "incident" + TYPE_QUESTION = "question" + TYPE_TASK = "task" + + # Group: 3rd Line--Notify Support + NOTIFY_GROUP_ID = 360000036529 + # Organization: GDS + NOTIFY_ORG_ID = 21891972 + NOTIFY_TICKET_FORM_ID = 1900000284794 + + def __init__( + self, + subject, + message, + ticket_type, + p1=False, + user_name=None, + user_email=None, + requester_sees_message_content=True, + technical_ticket=False, + ticket_categories=None, + org_id=None, + org_type=None, + service_id=None, + email_ccs=None, + ): + self.subject = subject + self.message = message + self.ticket_type = ticket_type + self.p1 = p1 + self.user_name = user_name + self.user_email = user_email + self.requester_sees_message_content = requester_sees_message_content + self.technical_ticket = technical_ticket + self.ticket_categories = ticket_categories or [] + self.org_id = org_id + self.org_type = org_type + self.service_id = service_id + self.email_ccs = email_ccs + + @property + def request_data(self): + data = { + "ticket": { + "subject": self.subject, + "comment": { + "body": self.message, + "public": self.requester_sees_message_content, + }, + "group_id": self.NOTIFY_GROUP_ID, + "organization_id": self.NOTIFY_ORG_ID, + "ticket_form_id": self.NOTIFY_TICKET_FORM_ID, + "priority": self.PRIORITY_URGENT if self.p1 else self.PRIORITY_NORMAL, + "tags": [self.TAGS_P1 if self.p1 else self.TAGS_P2], + "type": self.ticket_type, + "custom_fields": self._get_custom_fields(), + } + } + + if self.email_ccs: + data["ticket"]["email_ccs"] = [ + {"user_email": email, "action": "put"} for email in self.email_ccs + ] + + # if no requester provided, then the call came from within Notify đŸ‘ģ + if self.user_email: + data["ticket"]["requester"] = { + "email": self.user_email, + "name": self.user_name or "(no name supplied)", + } + + return data + + def _get_custom_fields(self): + technical_ticket_tag = ( + f'notify_ticket_type_{"" if self.technical_ticket else "non_"}technical' + ) + org_type_tag = f"notify_org_type_{self.org_type}" if self.org_type else None + + return [ + { + "id": "1900000744994", + "value": technical_ticket_tag, + }, # Notify Ticket type field + { + "id": "360022836500", + "value": self.ticket_categories, + }, # Notify Ticket category field + { + "id": "360022943959", + "value": self.org_id, + }, # Notify Organisation ID field + { + "id": "360022943979", + "value": org_type_tag, + }, # Notify Organisation type field + { + "id": "1900000745014", + "value": self.service_id, + }, # Notify Service ID field + ] diff --git a/notifications_utils/countries/__init__.py b/notifications_utils/countries/__init__.py new file mode 100644 index 000000000..ccd4a4e30 --- /dev/null +++ b/notifications_utils/countries/__init__.py @@ -0,0 +1,81 @@ +from functools import lru_cache + +from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.sanitise_text import SanitiseASCII + +from .data import ( + ADDITIONAL_SYNONYMS, + COUNTRIES_AND_TERRITORIES, + EUROPEAN_ISLANDS, + ROYAL_MAIL_EUROPEAN, + UK, + UK_ISLANDS, + WELSH_NAMES, + Postage, +) + + +class CountryMapping(InsensitiveDict): + @staticmethod + @lru_cache(maxsize=2048, typed=False) + def make_key(original_key): + original_key = original_key.replace("&", "and") + original_key = original_key.replace("+", "and") + + normalised = "".join( + character.lower() + for character in original_key + if character not in " _-'’,.()" + ) + + if "?" in SanitiseASCII.encode(normalised): + return normalised + + return SanitiseASCII.encode(normalised) + + def __contains__(self, key): + if any(c.isdigit() for c in key): + # A string with a digit can’t be a country and is probably a + # postcode, so let’s do a little optimisation, skip the + # expensive string manipulation to normalise the key and say + # that there’s no matching country + return False + return super().__contains__(key) + + def __getitem__(self, key): + for key_ in (key, f"the {key}", f"yr {key}", f"y {key}"): + if key_ in self: + return super().__getitem__(key_) + + raise CountryNotFoundError(f"Not a known country or territory ({key})") + + +countries = CountryMapping( + dict( + COUNTRIES_AND_TERRITORIES + + UK_ISLANDS + + EUROPEAN_ISLANDS + + WELSH_NAMES + + ADDITIONAL_SYNONYMS + ) +) + + +class Country: + def __init__(self, given_name): + self.canonical_name = countries[given_name] + + def __eq__(self, other): + return self.canonical_name == other.canonical_name + + @property + def postage_zone(self): + if self.canonical_name == UK: + return Postage.UK + if self.canonical_name in ROYAL_MAIL_EUROPEAN: + return Postage.EUROPE + return Postage.REST_OF_WORLD + + +class CountryNotFoundError(KeyError): + pass diff --git a/notifications_utils/countries/_data/ended-countries.json b/notifications_utils/countries/_data/ended-countries.json new file mode 100644 index 000000000..2d6011175 --- /dev/null +++ b/notifications_utils/countries/_data/ended-countries.json @@ -0,0 +1,6 @@ +{ + "Yugoslavia": null, + "USSR": null, + "East Germany": "Germany", + "Czechoslovakia": "Czechia" +} diff --git a/notifications_utils/countries/_data/europe.txt b/notifications_utils/countries/_data/europe.txt new file mode 100644 index 000000000..05e17e24f --- /dev/null +++ b/notifications_utils/countries/_data/europe.txt @@ -0,0 +1,62 @@ +Albania +Andorra +Armenia +Austria +Azerbaijan +Azores +Balearic Islands +Belarus +Belgium +Bosnia and Herzegovina +Bulgaria +Canary Islands +Corsica +Croatia +Cyprus +Czechia +Denmark +Estonia +Faroe Islands +Finland +France +Georgia +Germany +Gibraltar +Greece +Greenland +Hungary +Iceland +Ireland +Italy +Kazakhstan +Kosovo +Kyrgyzstan +Latvia +Liechtenstein +Lithuania +Luxembourg +North Macedonia +Madeira +Malta +Moldova +Monaco +Montenegro +Netherlands +Norway +Poland +Portugal +Romania +Russia +San Marino +Serbia +Slovakia +Slovenia +Spain +Sweden +Switzerland +Tajikistan +Turkey +Turkmenistan +Ukraine +Uzbekistan +Vatican City diff --git a/notifications_utils/countries/_data/european-islands.txt b/notifications_utils/countries/_data/european-islands.txt new file mode 100644 index 000000000..4899f327b --- /dev/null +++ b/notifications_utils/countries/_data/european-islands.txt @@ -0,0 +1,5 @@ +Azores +Balearic Islands +Canary Islands +Corsica +Madeira diff --git a/notifications_utils/countries/_data/location-autocomplete-graph.json b/notifications_utils/countries/_data/location-autocomplete-graph.json new file mode 100644 index 000000000..4313664d9 --- /dev/null +++ b/notifications_utils/countries/_data/location-autocomplete-graph.json @@ -0,0 +1,28607 @@ +{ + "country:AD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Andorra" + } + }, + "country:AE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "United Arab Emirates" + } + }, + "country:AF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Afghanistan" + } + }, + "country:AG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Antigua and Barbuda" + } + }, + "country:AL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Albania" + } + }, + "country:AM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Armenia" + } + }, + "country:AO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Angola" + } + }, + "country:AR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Argentina" + } + }, + "country:AT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Austria" + } + }, + "country:AU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Australia" + } + }, + "country:AZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Azerbaijan" + } + }, + "country:BA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bosnia and Herzegovina" + } + }, + "country:BB": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Barbados" + } + }, + "country:BD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bangladesh" + } + }, + "country:BE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Belgium" + } + }, + "country:BF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Burkina Faso" + } + }, + "country:BG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bulgaria" + } + }, + "country:BH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bahrain" + } + }, + "country:BI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Burundi" + } + }, + "country:BJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Benin" + } + }, + "country:BN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Brunei" + } + }, + "country:BO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bolivia" + } + }, + "country:BR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Brazil" + } + }, + "country:BS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Bahamas" + } + }, + "country:BT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bhutan" + } + }, + "country:BW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Botswana" + } + }, + "country:BY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Belarus" + } + }, + "country:BZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Belize" + } + }, + "country:CA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Canada" + } + }, + "country:CD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Congo (Democratic Republic)" + } + }, + "country:CF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Central African Republic" + } + }, + "country:CG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Congo" + } + }, + "country:CH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Switzerland" + } + }, + "country:CI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ivory Coast" + } + }, + "country:CL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Chile" + } + }, + "country:CM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cameroon" + } + }, + "country:CN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "China" + } + }, + "country:CO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Colombia" + } + }, + "country:CR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Costa Rica" + } + }, + "country:CS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Czechoslovakia" + } + }, + "country:CU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cuba" + } + }, + "country:CV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cape Verde" + } + }, + "country:CY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cyprus" + } + }, + "country:CZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Czechia" + } + }, + "country:DD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "East Germany" + } + }, + "country:DE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Germany" + } + }, + "country:DJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Djibouti" + } + }, + "country:DK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Denmark" + } + }, + "country:DM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Dominica" + } + }, + "country:DO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Dominican Republic" + } + }, + "country:DZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Algeria" + } + }, + "country:EC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ecuador" + } + }, + "country:EE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Estonia" + } + }, + "country:EG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Egypt" + } + }, + "country:ER": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Eritrea" + } + }, + "country:ES": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Spain" + } + }, + "country:ET": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ethiopia" + } + }, + "country:FI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Finland" + } + }, + "country:FJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Fiji" + } + }, + "country:FM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Micronesia" + } + }, + "country:FR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "France" + } + }, + "country:GA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Gabon" + } + }, + "country:GB": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "United Kingdom" + } + }, + "country:GD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Grenada" + } + }, + "country:GE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Georgia" + } + }, + "country:GH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ghana" + } + }, + "country:GM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Gambia" + } + }, + "country:GN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guinea" + } + }, + "country:GQ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Equatorial Guinea" + } + }, + "country:GR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Greece" + } + }, + "country:GT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guatemala" + } + }, + "country:GW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guinea-Bissau" + } + }, + "country:GY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guyana" + } + }, + "country:HN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Honduras" + } + }, + "country:HR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Croatia" + } + }, + "country:HT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Haiti" + } + }, + "country:HU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Hungary" + } + }, + "country:ID": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Indonesia" + } + }, + "country:IE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ireland" + } + }, + "country:IL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Israel" + } + }, + "country:IN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "India" + } + }, + "country:IQ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Iraq" + } + }, + "country:IR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Iran" + } + }, + "country:IS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Iceland" + } + }, + "country:IT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Italy" + } + }, + "country:JM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jamaica" + } + }, + "country:JO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jordan" + } + }, + "country:JP": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Japan" + } + }, + "country:KE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kenya" + } + }, + "country:KG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kyrgyzstan" + } + }, + "country:KH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cambodia" + } + }, + "country:KI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kiribati" + } + }, + "country:KM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Comoros" + } + }, + "country:KN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "St Kitts and Nevis" + } + }, + "country:KP": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "North Korea" + } + }, + "country:KR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "South Korea" + } + }, + "country:KW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kuwait" + } + }, + "country:KZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kazakhstan" + } + }, + "country:LA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Laos" + } + }, + "country:LB": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Lebanon" + } + }, + "country:LC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "St Lucia" + } + }, + "country:LI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Liechtenstein" + } + }, + "country:LK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sri Lanka" + } + }, + "country:LR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Liberia" + } + }, + "country:LS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Lesotho" + } + }, + "country:LT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Lithuania" + } + }, + "country:LU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Luxembourg" + } + }, + "country:LV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Latvia" + } + }, + "country:LY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Libya" + } + }, + "country:MA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Morocco" + } + }, + "country:MC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Monaco" + } + }, + "country:MD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Moldova" + } + }, + "country:ME": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Montenegro" + } + }, + "country:MG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Madagascar" + } + }, + "country:MH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Marshall Islands" + } + }, + "country:MK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "North Macedonia" + } + }, + "country:ML": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mali" + } + }, + "country:MM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Myanmar (Burma)" + } + }, + "country:MN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mongolia" + } + }, + "country:MR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mauritania" + } + }, + "country:MT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Malta" + } + }, + "country:MU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mauritius" + } + }, + "country:MV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Maldives" + } + }, + "country:MW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Malawi" + } + }, + "country:MX": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mexico" + } + }, + "country:MY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Malaysia" + } + }, + "country:MZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mozambique" + } + }, + "country:NA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Namibia" + } + }, + "country:NE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Niger" + } + }, + "country:NG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Nigeria" + } + }, + "country:NI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Nicaragua" + } + }, + "country:NL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Netherlands" + } + }, + "country:NO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Norway" + } + }, + "country:NP": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Nepal" + } + }, + "country:NR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Nauru" + } + }, + "country:NZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "New Zealand" + } + }, + "country:OM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Oman" + } + }, + "country:PA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Panama" + } + }, + "country:PE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Peru" + } + }, + "country:PG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Papua New Guinea" + } + }, + "country:PH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Philippines" + } + }, + "country:PK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Pakistan" + } + }, + "country:PL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Poland" + } + }, + "country:PT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Portugal" + } + }, + "country:PW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Palau" + } + }, + "country:PY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Paraguay" + } + }, + "country:QA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Qatar" + } + }, + "country:RO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Romania" + } + }, + "country:RS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Serbia" + } + }, + "country:RU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Russia" + } + }, + "country:RW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Rwanda" + } + }, + "country:SA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saudi Arabia" + } + }, + "country:SB": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Solomon Islands" + } + }, + "country:SC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Seychelles" + } + }, + "country:SD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sudan" + } + }, + "country:SE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sweden" + } + }, + "country:SG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Singapore" + } + }, + "country:SI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Slovenia" + } + }, + "country:SK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Slovakia" + } + }, + "country:SL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sierra Leone" + } + }, + "country:SM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "San Marino" + } + }, + "country:SN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Senegal" + } + }, + "country:SO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Somalia" + } + }, + "country:SR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Suriname" + } + }, + "country:SS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "South Sudan" + } + }, + "country:ST": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sao Tome and Principe" + } + }, + "country:SU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "USSR" + } + }, + "country:SV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "El Salvador" + } + }, + "country:SY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Syria" + } + }, + "country:SZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Eswatini" + } + }, + "country:TD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Chad" + } + }, + "country:TG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Togo" + } + }, + "country:TH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Thailand" + } + }, + "country:TJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tajikistan" + } + }, + "country:TL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "East Timor" + } + }, + "country:TM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Turkmenistan" + } + }, + "country:TN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tunisia" + } + }, + "country:TO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tonga" + } + }, + "country:TR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Turkey" + } + }, + "country:TT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Trinidad and Tobago" + } + }, + "country:TV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tuvalu" + } + }, + "country:TZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tanzania" + } + }, + "country:UA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ukraine" + } + }, + "country:UG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Uganda" + } + }, + "country:US": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "United States" + } + }, + "country:UY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Uruguay" + } + }, + "country:UZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Uzbekistan" + } + }, + "country:VA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Vatican City" + } + }, + "country:VC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "St Vincent" + } + }, + "country:VE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Venezuela" + } + }, + "country:VN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Vietnam" + } + }, + "country:VU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Vanuatu" + } + }, + "country:WS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Samoa" + } + }, + "country:XK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kosovo" + } + }, + "country:YE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Yemen" + } + }, + "country:YU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Yugoslavia" + } + }, + "country:ZA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "South Africa" + } + }, + "country:ZM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Zambia" + } + }, + "country:ZW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Zimbabwe" + } + }, + "nym:AD": { + "edges": { + "from": [ + "country:AD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AD" + } + }, + "nym:AE": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE" + } + }, + "nym:AE-AJ": { + "edges": { + "from": [ + "territory:AE-AJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-AJ" + } + }, + "nym:AE-AZ": { + "edges": { + "from": [ + "territory:AE-AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-AZ" + } + }, + "nym:AE-DU": { + "edges": { + "from": [ + "territory:AE-DU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-DU" + } + }, + "nym:AE-FU": { + "edges": { + "from": [ + "territory:AE-FU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-FU" + } + }, + "nym:AE-RK": { + "edges": { + "from": [ + "territory:AE-RK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-RK" + } + }, + "nym:AE-SH": { + "edges": { + "from": [ + "territory:AE-SH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-SH" + } + }, + "nym:AE-UQ": { + "edges": { + "from": [ + "territory:AE-UQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AE-UQ" + } + }, + "nym:AF": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AF" + } + }, + "nym:AG": { + "edges": { + "from": [ + "country:AG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AG" + } + }, + "nym:AI": { + "edges": { + "from": [ + "territory:AI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AI" + } + }, + "nym:AL": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AL" + } + }, + "nym:AM": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AM" + } + }, + "nym:AO": { + "edges": { + "from": [ + "country:AO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AO" + } + }, + "nym:AQ": { + "edges": { + "from": [ + "territory:AQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AQ" + } + }, + "nym:AR": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AR" + } + }, + "nym:AS": { + "edges": { + "from": [ + "territory:AS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AS" + } + }, + "nym:AT": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AT" + } + }, + "nym:AU": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AU" + } + }, + "nym:AW": { + "edges": { + "from": [ + "territory:AW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AW" + } + }, + "nym:AX": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AX" + } + }, + "nym:AZ": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "AZ" + } + }, + "nym:Aaland": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aaland" + } + }, + "nym:Abyssinia": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Abyssinia" + } + }, + "nym:Aeroes": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aeroes" + } + }, + "nym:Afghanestan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Afghanestan" + } + }, + "nym:Aforika Borwa": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aforika Borwa" + } + }, + "nym:Afrika Borwa": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Afrika Borwa" + } + }, + "nym:Afrika Dzonga": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Afrika Dzonga" + } + }, + "nym:Afrika-Borwa": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Afrika-Borwa" + } + }, + "nym:Afurika Tshipembe": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Afurika Tshipembe" + } + }, + "nym:Agawec": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Agawec" + } + }, + "nym:Ahvenanmaa": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ahvenanmaa" + } + }, + "nym:Al itihaad al islamiya": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al itihaad al islamiya" + } + }, + "nym:Al-'Iraq": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-'Iraq" + } + }, + "nym:Al-Baá¸Ĩrayn": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-Baá¸Ĩrayn" + } + }, + "nym:Al-Iraq": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-Iraq" + } + }, + "nym:Al-Jazā'ir": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-Jazā'ir" + } + }, + "nym:Al-Mamlaka Al-‘Arabiyyah as Sa‘ÅĢdiyyah": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-Mamlaka Al-‘Arabiyyah as Sa‘ÅĢdiyyah" + } + }, + "nym:Al-Yaman": { + "edges": { + "from": [ + "country:YE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-Yaman" + } + }, + "nym:Al-itihaad al-islamiya": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-itihaad al-islamiya" + } + }, + "nym:Al-maÉŖrÊb": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-maÉŖrÊb" + } + }, + "nym:Al-’Imārat Al-‘Arabiyyah Al-Muttaá¸Ĩidah": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-’Imārat Al-‘Arabiyyah Al-Muttaá¸Ĩidah" + } + }, + "nym:Al-’Urdun": { + "edges": { + "from": [ + "country:JO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Al-’Urdun" + } + }, + "nym:Aland": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aland" + } + }, + "nym:Ameeri": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ameeri" + } + }, + "nym:Ameica": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ameica" + } + }, + "nym:Amelika-hui-pu-'ia": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amelika-hui-pu-'ia" + } + }, + "nym:Amerca": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amerca" + } + }, + "nym:Amercia": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amercia" + } + }, + "nym:Ameria": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ameria" + } + }, + "nym:American Virgin Islands": { + "edges": { + "from": [ + "territory:VI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "American Virgin Islands" + } + }, + "nym:Amerika Sāmoa": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amerika Sāmoa" + } + }, + "nym:Amerruk": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amerruk" + } + }, + "nym:Amrica": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Amrica" + } + }, + "nym:An Rywvaneth Unys": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "An Rywvaneth Unys" + } + }, + "nym:Anguilla": { + "edges": { + "from": [ + "territory:AI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Anguilla" + } + }, + "nym:Anmerica": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Anmerica" + } + }, + "nym:Annam": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Annam" + } + }, + "nym:Antarctica": { + "edges": { + "from": [ + "territory:AQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Antarctica" + } + }, + "nym:Antigua and Barbuda": { + "edges": { + "from": [ + "country:AG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Antigua and Barbuda" + } + }, + "nym:Aorōkin M˧ajeÄŧ": { + "edges": { + "from": [ + "country:MH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aorōkin M˧ajeÄŧ" + } + }, + "nym:Aotearoa": { + "edges": { + "from": [ + "country:NZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Aotearoa" + } + }, + "nym:Arab Republic of Egypt": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Arab Republic of Egypt" + } + }, + "nym:Argenina": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Argenina" + } + }, + "nym:Argentine Republic": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Argentine Republic" + } + }, + "nym:Argentinia": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Argentinia" + } + }, + "nym:Aruba": { + "edges": { + "from": [ + "territory:AW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Aruba" + } + }, + "nym:As-Sudan": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "As-Sudan" + } + }, + "nym:Ascension Island": { + "edges": { + "from": [ + "territory:SH-AC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ascension Island" + } + }, + "nym:Ayiti": { + "edges": { + "from": [ + "country:HT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ayiti" + } + }, + "nym:Azerbaijani Republic": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Azerbaijani Republic" + } + }, + "nym:Azerbajdzhan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Azerbajdzhan" + } + }, + "nym:Azerbajdzhan Republic": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Azerbajdzhan Republic" + } + }, + "nym:Azərbaycan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Azərbaycan" + } + }, + "nym:B.V.I.": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "B.V.I." + } + }, + "nym:BA": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BA" + } + }, + "nym:BAT": { + "edges": { + "from": [ + "territory:BAT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BAT" + } + }, + "nym:BB": { + "edges": { + "from": [ + "country:BB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BB" + } + }, + "nym:BD": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BD" + } + }, + "nym:BE": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BE" + } + }, + "nym:BF": { + "edges": { + "from": [ + "country:BF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BF" + } + }, + "nym:BG": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BG" + } + }, + "nym:BH": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BH" + } + }, + "nym:BI": { + "edges": { + "from": [ + "country:BI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BI" + } + }, + "nym:BJ": { + "edges": { + "from": [ + "country:BJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BJ" + } + }, + "nym:BL": { + "edges": { + "from": [ + "territory:BL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BL" + } + }, + "nym:BM": { + "edges": { + "from": [ + "territory:BM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BM" + } + }, + "nym:BN": { + "edges": { + "from": [ + "country:BN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BN" + } + }, + "nym:BO": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BO" + } + }, + "nym:BQ-BO": { + "edges": { + "from": [ + "territory:BQ-BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BQ-BO" + } + }, + "nym:BQ-SA": { + "edges": { + "from": [ + "territory:BQ-SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BQ-SA" + } + }, + "nym:BQ-SE": { + "edges": { + "from": [ + "territory:BQ-SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BQ-SE" + } + }, + "nym:BR": { + "edges": { + "from": [ + "country:BR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BR" + } + }, + "nym:BS": { + "edges": { + "from": [ + "country:BS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BS" + } + }, + "nym:BT": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BT" + } + }, + "nym:BV": { + "edges": { + "from": [ + "territory:BV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BV" + } + }, + "nym:BVI": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BVI" + } + }, + "nym:BW": { + "edges": { + "from": [ + "country:BW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BW" + } + }, + "nym:BY": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BY" + } + }, + "nym:BZ": { + "edges": { + "from": [ + "country:BZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BZ" + } + }, + "nym:Bailiwick of Guernsey": { + "edges": { + "from": [ + "territory:GG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bailiwick of Guernsey" + } + }, + "nym:Bailiwick of Jersey": { + "edges": { + "from": [ + "territory:JE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bailiwick of Jersey" + } + }, + "nym:Baker Island": { + "edges": { + "from": [ + "territory:UM-81" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Baker Island" + } + }, + "nym:Bangla Desh": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bangla Desh" + } + }, + "nym:Barbados": { + "edges": { + "from": [ + "country:BB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Barbados" + } + }, + "nym:Basutoland": { + "edges": { + "from": [ + "country:LS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Basutoland" + } + }, + "nym:Belarus": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belarus" + } + }, + "nym:Belau": { + "edges": { + "from": [ + "country:PW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belau" + } + }, + "nym:Belgie": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belgie" + } + }, + "nym:Belgien": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belgien" + } + }, + "nym:Belgique": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belgique" + } + }, + "nym:BelgiÃĢ": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BelgiÃĢ" + } + }, + "nym:Belguim": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Belguim" + } + }, + "nym:Belize": { + "edges": { + "from": [ + "country:BZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Belize" + } + }, + "nym:Bermuda": { + "edges": { + "from": [ + "territory:BM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bermuda" + } + }, + "nym:Bermudas": { + "edges": { + "from": [ + "territory:BM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bermudas" + } + }, + "nym:Bharat": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bharat" + } + }, + "nym:Bharôt": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bharôt" + } + }, + "nym:Bharôtô": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bharôtô" + } + }, + "nym:Bhārat": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bhārat" + } + }, + "nym:Bhārata": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bhārata" + } + }, + "nym:Bhāratadēsam": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bhāratadēsam" + } + }, + "nym:Bhāratam": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bhāratam" + } + }, + "nym:BiH": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "BiH" + } + }, + "nym:Bielaruś": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bielaruś" + } + }, + "nym:Bitain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bitain" + } + }, + "nym:Bolivarian Republic of Venezuela": { + "edges": { + "from": [ + "country:VE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bolivarian Republic of Venezuela" + } + }, + "nym:Bolivia": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bolivia" + } + }, + "nym:Bonaire": { + "edges": { + "from": [ + "territory:BQ-BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bonaire" + } + }, + "nym:Bosna i Hercegovina": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bosna i Hercegovina" + } + }, + "nym:Bosnia and Herzegovina": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bosnia and Herzegovina" + } + }, + "nym:Bosnia-Herzegovina": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bosnia-Herzegovina" + } + }, + "nym:Bouvet Island": { + "edges": { + "from": [ + "territory:BV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bouvet Island" + } + }, + "nym:Brasil": { + "edges": { + "from": [ + "country:BR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Brasil" + } + }, + "nym:Brazzaville": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Brazzaville" + } + }, + "nym:Briain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Briain" + } + }, + "nym:Britain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Britain" + } + }, + "nym:Britiain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Britiain" + } + }, + "nym:British Antarctic Territory": { + "edges": { + "from": [ + "territory:BAT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "British Antarctic Territory" + } + }, + "nym:British Guiana": { + "edges": { + "from": [ + "country:GY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "British Guiana" + } + }, + "nym:British Honduras": { + "edges": { + "from": [ + "country:BZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "British Honduras" + } + }, + "nym:Brtain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Brtain" + } + }, + "nym:Brunei Darussalam": { + "edges": { + "from": [ + "country:BN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Brunei Darussalam" + } + }, + "nym:Bugaria": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bugaria" + } + }, + "nym:Bukchosŏn": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bukchosŏn" + } + }, + "nym:Bulagar": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bulagar" + } + }, + "nym:Bulgariya": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bulgariya" + } + }, + "nym:Buliwya": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Buliwya" + } + }, + "nym:Bundesrepublik": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bundesrepublik" + } + }, + "nym:Burkina Faso": { + "edges": { + "from": [ + "country:BF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Burkina Faso" + } + }, + "nym:Burkina Fasoupper": { + "edges": { + "from": [ + "country:BF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Burkina Fasoupper" + } + }, + "nym:Byelarus": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Byelarus" + } + }, + "nym:Byelorussia": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Byelorussia" + } + }, + "nym:Bārata": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bārata" + } + }, + "nym:Bălgarija": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Bălgarija" + } + }, + "nym:CA": { + "edges": { + "from": [ + "country:CA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CA" + } + }, + "nym:CAR": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CAR" + } + }, + "nym:CC": { + "edges": { + "from": [ + "territory:CC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CC" + } + }, + "nym:CD": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CD" + } + }, + "nym:CF": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CF" + } + }, + "nym:CG": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CG" + } + }, + "nym:CH": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CH" + } + }, + "nym:CI": { + "edges": { + "from": [ + "country:CI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CI" + } + }, + "nym:CK": { + "edges": { + "from": [ + "territory:CK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CK" + } + }, + "nym:CL": { + "edges": { + "from": [ + "country:CL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CL" + } + }, + "nym:CM": { + "edges": { + "from": [ + "country:CM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CM" + } + }, + "nym:CN": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CN" + } + }, + "nym:CO": { + "edges": { + "from": [ + "country:CO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CO" + } + }, + "nym:CR": { + "edges": { + "from": [ + "country:CR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CR" + } + }, + "nym:CS": { + "edges": { + "from": [ + "country:CS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CS" + } + }, + "nym:CU": { + "edges": { + "from": [ + "country:CU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CU" + } + }, + "nym:CV": { + "edges": { + "from": [ + "country:CV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CV" + } + }, + "nym:CW": { + "edges": { + "from": [ + "territory:CW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CW" + } + }, + "nym:CX": { + "edges": { + "from": [ + "territory:CX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CX" + } + }, + "nym:CY": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CY" + } + }, + "nym:CZ": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "CZ" + } + }, + "nym:Cabo": { + "edges": { + "from": [ + "country:CV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cabo" + } + }, + "nym:Cabo Verde": { + "edges": { + "from": [ + "country:CV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cabo Verde" + } + }, + "nym:Cameroon": { + "edges": { + "from": [ + "country:CM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cameroon" + } + }, + "nym:Cameroun": { + "edges": { + "from": [ + "country:CM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cameroun" + } + }, + "nym:Canada": { + "edges": { + "from": [ + "country:CA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Canada" + } + }, + "nym:Canadaigua": { + "edges": { + "from": [ + "country:CA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Canadaigua" + } + }, + "nym:Candada": { + "edges": { + "from": [ + "country:CA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Candada" + } + }, + "nym:Cathay": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cathay" + } + }, + "nym:Cayman Islands": { + "edges": { + "from": [ + "territory:KY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cayman Islands" + } + }, + "nym:Central Africa": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Central Africa" + } + }, + "nym:Central African Republic": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Central African Republic" + } + }, + "nym:Ceska": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ceska" + } + }, + "nym:Ceuta": { + "edges": { + "from": [ + "territory:ES-CE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ceuta" + } + }, + "nym:Ceylon": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ceylon" + } + }, + "nym:Chinese Taipei": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Chinese Taipei" + } + }, + "nym:Citta del Vaticano": { + "edges": { + "from": [ + "country:VA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Citta del Vaticano" + } + }, + "nym:Co-operative Republic of Guyana": { + "edges": { + "from": [ + "country:GY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Co-operative Republic of Guyana" + } + }, + "nym:Coasta Rica": { + "edges": { + "from": [ + "country:CR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Coasta Rica" + } + }, + "nym:Collectivity of Saint Martin": { + "edges": { + "from": [ + "territory:MF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Collectivity of Saint Martin" + } + }, + "nym:Commonwealth of Australia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Commonwealth of Australia" + } + }, + "nym:Commonwealth of Bahamas": { + "edges": { + "from": [ + "country:BS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Commonwealth of Bahamas" + } + }, + "nym:Commonwealth of Dominica": { + "edges": { + "from": [ + "country:DM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Commonwealth of Dominica" + } + }, + "nym:Commonwealth of Puerto Rico": { + "edges": { + "from": [ + "territory:PR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Commonwealth of Puerto Rico" + } + }, + "nym:Commonwealth of the Northern Mariana Islands": { + "edges": { + "from": [ + "territory:MP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Commonwealth of the Northern Mariana Islands" + } + }, + "nym:Comores": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Comores" + } + }, + "nym:Congo-Brazzaville": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Congo-Brazzaville" + } + }, + "nym:Cook Islands": { + "edges": { + "from": [ + "territory:CK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cook Islands" + } + }, + "nym:Costa Rico": { + "edges": { + "from": [ + "country:CR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Costa Rico" + } + }, + "nym:Cote D'Ivoire": { + "edges": { + "from": [ + "country:CI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cote D'Ivoire" + } + }, + "nym:Cote dIvoire": { + "edges": { + "from": [ + "country:CI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Cote dIvoire" + } + }, + "nym:Country of Curaçao": { + "edges": { + "from": [ + "territory:CW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Country of Curaçao" + } + }, + "nym:Crna Gora": { + "edges": { + "from": [ + "country:ME" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Crna Gora" + } + }, + "nym:Curacao": { + "edges": { + "from": [ + "territory:CW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Curacao" + } + }, + "nym:Curaçao": { + "edges": { + "from": [ + "territory:CW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Curaçao" + } + }, + "nym:Czech Republic": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Czech Republic" + } + }, + "nym:Czechoslav": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Czechoslav" + } + }, + "nym:Czechoslovak Republic": { + "edges": { + "from": [ + "country:CS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Czechoslovak Republic" + } + }, + "nym:DD": { + "edges": { + "from": [ + "country:DD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DD" + } + }, + "nym:DE": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DE" + } + }, + "nym:DJ": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DJ" + } + }, + "nym:DK": { + "edges": { + "from": [ + "country:DK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DK" + } + }, + "nym:DM": { + "edges": { + "from": [ + "country:DM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DM" + } + }, + "nym:DO": { + "edges": { + "from": [ + "country:DO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DO" + } + }, + "nym:DPRK": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DPRK" + } + }, + "nym:DZ": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "DZ" + } + }, + "nym:Dahomey": { + "edges": { + "from": [ + "country:BJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dahomey" + } + }, + "nym:Danmark": { + "edges": { + "from": [ + "country:DK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Danmark" + } + }, + "nym:Dawlat ul-Kuwayt": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dawlat ul-Kuwayt" + } + }, + "nym:Democratic People's Republic of Koread": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Democratic People's Republic of Koread" + } + }, + "nym:Democratic Republic of Sao Tome and Principe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Democratic Republic of Sao Tome and Principe" + } + }, + "nym:Democratic Republic of Timor-Lestetimor": { + "edges": { + "from": [ + "country:TL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Democratic Republic of Timor-Lestetimor" + } + }, + "nym:Democratic Republic of the Congo": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Democratic Republic of the Congo" + } + }, + "nym:Democratic Socialist Republic of Sri Lanka": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Democratic Socialist Republic of Sri Lanka" + } + }, + "nym:Deutschland": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Deutschland" + } + }, + "nym:Dhivehi Raajje": { + "edges": { + "from": [ + "country:MV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dhivehi Raajje" + } + }, + "nym:Djibouti": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Djibouti" + } + }, + "nym:Dominican Republic": { + "edges": { + "from": [ + "country:DO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dominican Republic" + } + }, + "nym:Dominique": { + "edges": { + "from": [ + "country:DM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dominique" + } + }, + "nym:Druk Yul": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Druk Yul" + } + }, + "nym:Ducie and Oeno Islands": { + "edges": { + "from": [ + "territory:PN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ducie and Oeno Islands" + } + }, + "nym:Dutch East Indies": { + "edges": { + "from": [ + "country:ID" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dutch East Indies" + } + }, + "nym:Dzayer": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Dzayer" + } + }, + "nym:E Civitate Vaticana": { + "edges": { + "from": [ + "country:VA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "E Civitate Vaticana" + } + }, + "nym:EC": { + "edges": { + "from": [ + "country:EC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "EC" + } + }, + "nym:EE": { + "edges": { + "from": [ + "country:EE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "EE" + } + }, + "nym:EG": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "EG" + } + }, + "nym:EH": { + "edges": { + "from": [ + "territory:EH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "EH" + } + }, + "nym:ENG": { + "edges": { + "from": [ + "uk:ENG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ENG" + } + }, + "nym:ER": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ER" + } + }, + "nym:ES": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ES" + } + }, + "nym:ES-CE": { + "edges": { + "from": [ + "territory:ES-CE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ES-CE" + } + }, + "nym:ES-ML": { + "edges": { + "from": [ + "territory:ES-ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ES-ML" + } + }, + "nym:ET": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ET" + } + }, + "nym:East Pakistan": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "East Pakistan" + } + }, + "nym:Eastern Samoa": { + "edges": { + "from": [ + "territory:AS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Eastern Samoa" + } + }, + "nym:Eesti": { + "edges": { + "from": [ + "country:EE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Eesti" + } + }, + "nym:Egpyt": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Egpyt" + } + }, + "nym:Egyot": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Egyot" + } + }, + "nym:Egyt": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Egyt" + } + }, + "nym:Eire": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Eire" + } + }, + "nym:Ellada": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ellada" + } + }, + "nym:Ellan Vannin": { + "edges": { + "from": [ + "territory:IM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ellan Vannin" + } + }, + "nym:Ellas": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ellas" + } + }, + "nym:Ellice Islands": { + "edges": { + "from": [ + "country:TV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ellice Islands" + } + }, + "nym:ElmeÉŖrib": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ElmeÉŖrib" + } + }, + "nym:Emirate of Abu Dhabi": { + "edges": { + "from": [ + "territory:AE-AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Abu Dhabi" + } + }, + "nym:Emirate of Ajman": { + "edges": { + "from": [ + "territory:AE-AJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Ajman" + } + }, + "nym:Emirate of Dubai": { + "edges": { + "from": [ + "territory:AE-DU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Dubai" + } + }, + "nym:Emirate of Fujairah": { + "edges": { + "from": [ + "territory:AE-FU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Fujairah" + } + }, + "nym:Emirate of Ras al-Khaimah": { + "edges": { + "from": [ + "territory:AE-RK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Ras al-Khaimah" + } + }, + "nym:Emirate of Sharjah": { + "edges": { + "from": [ + "territory:AE-SH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Sharjah" + } + }, + "nym:Emirate of Umm al-Quwain": { + "edges": { + "from": [ + "territory:AE-UQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Emirate of Umm al-Quwain" + } + }, + "nym:England": { + "edges": { + "from": [ + "uk:ENG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "England" + } + }, + "nym:Englsnd": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Englsnd" + } + }, + "nym:Ertra": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ertra" + } + }, + "nym:Espainia": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Espainia" + } + }, + "nym:Espanha": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Espanha" + } + }, + "nym:Espanya": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Espanya" + } + }, + "nym:EspaÃąa": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "EspaÃąa" + } + }, + "nym:Estados Unidos": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Estados Unidos" + } + }, + "nym:Esthonia": { + "edges": { + "from": [ + "country:EE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Esthonia" + } + }, + "nym:Eswatini": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Eswatini" + } + }, + "nym:Ethopi": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ethopi" + } + }, + "nym:FI": { + "edges": { + "from": [ + "country:FI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FI" + } + }, + "nym:FJ": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FJ" + } + }, + "nym:FK": { + "edges": { + "from": [ + "territory:FK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FK" + } + }, + "nym:FM": { + "edges": { + "from": [ + "country:FM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FM" + } + }, + "nym:FO": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FO" + } + }, + "nym:FR": { + "edges": { + "from": [ + "country:FR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FR" + } + }, + "nym:FRG": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FRG" + } + }, + "nym:Falkland Islands": { + "edges": { + "from": [ + "territory:FK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Falkland Islands" + } + }, + "nym:Faroe Islands": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Faroe Islands" + } + }, + "nym:Faroes": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Faroes" + } + }, + "nym:Federal Democratic Republic of Ethiopia": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Democratic Republic of Ethiopia" + } + }, + "nym:Federal Democratic Republic of Nepal": { + "edges": { + "from": [ + "country:NP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Democratic Republic of Nepal" + } + }, + "nym:Federal Islamic Republic of the Comoros": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Islamic Republic of the Comoros" + } + }, + "nym:Federal Republic of Germany": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Republic of Germany" + } + }, + "nym:Federal Republic of Nigeria": { + "edges": { + "from": [ + "country:NG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Republic of Nigeria" + } + }, + "nym:Federal Republic of Somalia": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Federal Republic of Somalia" + } + }, + "nym:Federal Republic of Somaliaaiai": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federal Republic of Somaliaaiai" + } + }, + "nym:Federated States of Micronesia": { + "edges": { + "from": [ + "country:FM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federated States of Micronesia" + } + }, + "nym:Federation of Malaysia": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federation of Malaysia" + } + }, + "nym:Federation of Saint Christopher and Nevis": { + "edges": { + "from": [ + "country:KN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federation of Saint Christopher and Nevis" + } + }, + "nym:Federative Republic of Brazil": { + "edges": { + "from": [ + "country:BR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Federative Republic of Brazil" + } + }, + "nym:Fiji": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Fiji" + } + }, + "nym:French Congo": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "French Congo" + } + }, + "nym:French Guiana": { + "edges": { + "from": [ + "territory:GF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Guiana" + } + }, + "nym:French Guinea": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "French Guinea" + } + }, + "nym:French Oceania": { + "edges": { + "from": [ + "territory:PF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "French Oceania" + } + }, + "nym:French Polynesia": { + "edges": { + "from": [ + "territory:PF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Polynesia" + } + }, + "nym:French Republic": { + "edges": { + "from": [ + "country:FR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "French Republic" + } + }, + "nym:French Southern Territories": { + "edges": { + "from": [ + "territory:TF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Southern Territories" + } + }, + "nym:French Sudan": { + "edges": { + "from": [ + "country:ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "French Sudan" + } + }, + "nym:Friendly islands": { + "edges": { + "from": [ + "country:TO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Friendly islands" + } + }, + "nym:FÃĻrøerne": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "FÃĻrøerne" + } + }, + "nym:Føroyar": { + "edges": { + "from": [ + "territory:FO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Føroyar" + } + }, + "nym:GA": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GA" + } + }, + "nym:GB": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GB" + } + }, + "nym:GBN": { + "edges": { + "from": [ + "uk:GBN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GBN" + } + }, + "nym:GD": { + "edges": { + "from": [ + "country:GD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GD" + } + }, + "nym:GE": { + "edges": { + "from": [ + "country:GE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GE" + } + }, + "nym:GF": { + "edges": { + "from": [ + "territory:GF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GF" + } + }, + "nym:GG": { + "edges": { + "from": [ + "territory:GG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GG" + } + }, + "nym:GH": { + "edges": { + "from": [ + "country:GH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GH" + } + }, + "nym:GI": { + "edges": { + "from": [ + "territory:GI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GI" + } + }, + "nym:GL": { + "edges": { + "from": [ + "territory:GL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GL" + } + }, + "nym:GM": { + "edges": { + "from": [ + "country:GM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GM" + } + }, + "nym:GN": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GN" + } + }, + "nym:GP": { + "edges": { + "from": [ + "territory:GP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GP" + } + }, + "nym:GQ": { + "edges": { + "from": [ + "country:GQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GQ" + } + }, + "nym:GR": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GR" + } + }, + "nym:GS": { + "edges": { + "from": [ + "territory:GS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GS" + } + }, + "nym:GT": { + "edges": { + "from": [ + "country:GT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GT" + } + }, + "nym:GU": { + "edges": { + "from": [ + "territory:GU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GU" + } + }, + "nym:GW": { + "edges": { + "from": [ + "country:GW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GW" + } + }, + "nym:GY": { + "edges": { + "from": [ + "country:GY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GY" + } + }, + "nym:Gabonese Republic": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gabonese Republic" + } + }, + "nym:Gabun": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gabun" + } + }, + "nym:Gabuuti": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gabuuti" + } + }, + "nym:Genus Argentina": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Genus Argentina" + } + }, + "nym:Georgia": { + "edges": { + "from": [ + "country:GE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Georgia" + } + }, + "nym:Germany Democratic Republic": { + "edges": { + "from": [ + "country:DD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Germany Democratic Republic" + } + }, + "nym:Gibraltar": { + "edges": { + "from": [ + "territory:GI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Gibraltar" + } + }, + "nym:Gine": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gine" + } + }, + "nym:Gold Coast": { + "edges": { + "from": [ + "country:GH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gold Coast" + } + }, + "nym:Grand Duchy of Luxembourg": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Grand Duchy of Luxembourg" + } + }, + "nym:Grat Britain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Grat Britain" + } + }, + "nym:Great Britain": { + "edges": { + "from": [ + "uk:GBN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Great Britain" + } + }, + "nym:Great Britan": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Great Britan" + } + }, + "nym:Greenland": { + "edges": { + "from": [ + "territory:GL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Greenland" + } + }, + "nym:Grenada": { + "edges": { + "from": [ + "country:GD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Grenada" + } + }, + "nym:Gret Britain": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gret Britain" + } + }, + "nym:Gronland": { + "edges": { + "from": [ + "territory:GL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Gronland" + } + }, + "nym:Grønland": { + "edges": { + "from": [ + "territory:GL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Grønland" + } + }, + "nym:Guadeloupe": { + "edges": { + "from": [ + "territory:GP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guadeloupe" + } + }, + "nym:Guinea Ecuatorial": { + "edges": { + "from": [ + "country:GQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Guinea Ecuatorial" + } + }, + "nym:GuinÊe": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GuinÊe" + } + }, + "nym:Guyane": { + "edges": { + "from": [ + "territory:GF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Guyane" + } + }, + "nym:GuÃĨhÃĨn": { + "edges": { + "from": [ + "territory:GU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "GuÃĨhÃĨn" + } + }, + "nym:HK": { + "edges": { + "from": [ + "territory:HK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HK" + } + }, + "nym:HM": { + "edges": { + "from": [ + "territory:HM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HM" + } + }, + "nym:HN": { + "edges": { + "from": [ + "country:HN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HN" + } + }, + "nym:HR": { + "edges": { + "from": [ + "country:HR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HR" + } + }, + "nym:HT": { + "edges": { + "from": [ + "country:HT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HT" + } + }, + "nym:HU": { + "edges": { + "from": [ + "country:HU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HU" + } + }, + "nym:Hanguk": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hanguk" + } + }, + "nym:Hashemite Kingdom of Jordan": { + "edges": { + "from": [ + "country:JO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hashemite Kingdom of Jordan" + } + }, + "nym:Hayastan": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hayastan" + } + }, + "nym:HayastÃĄn": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "HayastÃĄn" + } + }, + "nym:Haïti": { + "edges": { + "from": [ + "country:HT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Haïti" + } + }, + "nym:Hellas": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hellas" + } + }, + "nym:Hellenic Republic": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hellenic Republic" + } + }, + "nym:Henderson": { + "edges": { + "from": [ + "territory:PN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Henderson" + } + }, + "nym:Heung Gong": { + "edges": { + "from": [ + "territory:HK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Heung Gong" + } + }, + "nym:Hindustan": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hindustan" + } + }, + "nym:Holland": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Holland" + } + }, + "nym:Holy See": { + "edges": { + "from": [ + "country:VA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Holy See" + } + }, + "nym:Hong Kong Special Administrative Region": { + "edges": { + "from": [ + "territory:HK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Hong Kong Special Administrative Region" + } + }, + "nym:Howland Island": { + "edges": { + "from": [ + "territory:UM-84" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Howland Island" + } + }, + "nym:Hrvatska": { + "edges": { + "from": [ + "country:HR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Hrvatska" + } + }, + "nym:Hungary": { + "edges": { + "from": [ + "country:HU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Hungary" + } + }, + "nym:I.O.T.": { + "edges": { + "from": [ + "territory:IO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "I.O.T." + } + }, + "nym:ID": { + "edges": { + "from": [ + "country:ID" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ID" + } + }, + "nym:IE": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IE" + } + }, + "nym:IL": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IL" + } + }, + "nym:IM": { + "edges": { + "from": [ + "territory:IM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IM" + } + }, + "nym:IN": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IN" + } + }, + "nym:IO": { + "edges": { + "from": [ + "territory:IO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IO" + } + }, + "nym:IOT": { + "edges": { + "from": [ + "territory:IO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IOT" + } + }, + "nym:IQ": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IQ" + } + }, + "nym:IR": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IR" + } + }, + "nym:IS": { + "edges": { + "from": [ + "country:IS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IS" + } + }, + "nym:IT": { + "edges": { + "from": [ + "country:IT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IT" + } + }, + "nym:Independent State of Papua New Guinea": { + "edges": { + "from": [ + "country:PG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Independent State of Papua New Guinea" + } + }, + "nym:Independent State of Samoa": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Independent State of Samoa" + } + }, + "nym:Irak": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Irak" + } + }, + "nym:Ireland": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ireland" + } + }, + "nym:Irelend": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Irelend" + } + }, + "nym:Irish Republic": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Irish Republic" + } + }, + "nym:Iritriya": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Iritriya" + } + }, + "nym:Islamic Republic of Afghanistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Islamic Republic of Afghanistan" + } + }, + "nym:Islamic Republic of Gambia": { + "edges": { + "from": [ + "country:GM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Islamic Republic of Gambia" + } + }, + "nym:Islamic Republic of Iran": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Islamic Republic of Iran" + } + }, + "nym:Islamic Republic of Mauritania": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Islamic Republic of Mauritania" + } + }, + "nym:Islamic Republic of Pakistan": { + "edges": { + "from": [ + "country:PK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Islamic Republic of Pakistan" + } + }, + "nym:Island": { + "edges": { + "from": [ + "country:IS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Island" + } + }, + "nym:Island of Guernsey": { + "edges": { + "from": [ + "territory:GG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Island of Guernsey" + } + }, + "nym:Island of Jersey": { + "edges": { + "from": [ + "territory:JE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Island of Jersey" + } + }, + "nym:Isle of Man": { + "edges": { + "from": [ + "territory:IM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Isle of Man" + } + }, + "nym:IsraĘŧiyl": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "IsraĘŧiyl" + } + }, + "nym:Isreal": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Isreal" + } + }, + "nym:Italia": { + "edges": { + "from": [ + "country:IT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Italia" + } + }, + "nym:Italian Republic": { + "edges": { + "from": [ + "country:IT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Italian Republic" + } + }, + "nym:Itlay": { + "edges": { + "from": [ + "country:IT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Itlay" + } + }, + "nym:Ityop'ia": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ityop'ia" + } + }, + "nym:JE": { + "edges": { + "from": [ + "territory:JE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "JE" + } + }, + "nym:JM": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "JM" + } + }, + "nym:JO": { + "edges": { + "from": [ + "country:JO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "JO" + } + }, + "nym:JP": { + "edges": { + "from": [ + "country:JP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "JP" + } + }, + "nym:Jabuuti": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jabuuti" + } + }, + "nym:Jamaca": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jamaca" + } + }, + "nym:Jamacia": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jamacia" + } + }, + "nym:Jamahiriya": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jamahiriya" + } + }, + "nym:Jamaica": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jamaica" + } + }, + "nym:Japan": { + "edges": { + "from": [ + "country:JP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Japan" + } + }, + "nym:Jarvis Island": { + "edges": { + "from": [ + "territory:UM-86" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jarvis Island" + } + }, + "nym:Johnston Atoll": { + "edges": { + "from": [ + "territory:UM-67" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Johnston Atoll" + } + }, + "nym:Jugoslavija": { + "edges": { + "from": [ + "country:YU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jugoslavija" + } + }, + "nym:Juzur al-Qamar": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Juzur al-Qamar" + } + }, + "nym:Jèrri": { + "edges": { + "from": [ + "country:TV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Jèrri" + } + }, + "nym:JÄĢbÅĢtÄĢ": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "JÄĢbÅĢtÄĢ" + } + }, + "nym:KE": { + "edges": { + "from": [ + "country:KE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KE" + } + }, + "nym:KG": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KG" + } + }, + "nym:KH": { + "edges": { + "from": [ + "country:KH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KH" + } + }, + "nym:KI": { + "edges": { + "from": [ + "country:KI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KI" + } + }, + "nym:KM": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KM" + } + }, + "nym:KN": { + "edges": { + "from": [ + "country:KN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KN" + } + }, + "nym:KP": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KP" + } + }, + "nym:KR": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KR" + } + }, + "nym:KW": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KW" + } + }, + "nym:KY": { + "edges": { + "from": [ + "territory:KY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KY" + } + }, + "nym:KZ": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KZ" + } + }, + "nym:Kalaallit Nunaat": { + "edges": { + "from": [ + "territory:GL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kalaallit Nunaat" + } + }, + "nym:Kampuchea": { + "edges": { + "from": [ + "country:KH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kampuchea" + } + }, + "nym:Katar": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Katar" + } + }, + "nym:Kazakh": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kazakh" + } + }, + "nym:KazakhstÃĄn": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KazakhstÃĄn" + } + }, + "nym:Kazakstan": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kazakstan" + } + }, + "nym:Kingdom of Bahrain": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Bahrain" + } + }, + "nym:Kingdom of Belgium": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Belgium" + } + }, + "nym:Kingdom of Bhutan": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Bhutan" + } + }, + "nym:Kingdom of Cambodia": { + "edges": { + "from": [ + "country:KH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Cambodia" + } + }, + "nym:Kingdom of Denmark": { + "edges": { + "from": [ + "country:DK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Denmark" + } + }, + "nym:Kingdom of Eswatini": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Eswatini" + } + }, + "nym:Kingdom of Lesotho": { + "edges": { + "from": [ + "country:LS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Lesotho" + } + }, + "nym:Kingdom of Moroccoal-Magrib": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Moroccoal-Magrib" + } + }, + "nym:Kingdom of Norway": { + "edges": { + "from": [ + "country:NO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Norway" + } + }, + "nym:Kingdom of Saudi Arabia": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Saudi Arabia" + } + }, + "nym:Kingdom of Spain": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Spain" + } + }, + "nym:Kingdom of Swaziland": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Swaziland" + } + }, + "nym:Kingdom of Sweden": { + "edges": { + "from": [ + "country:SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Sweden" + } + }, + "nym:Kingdom of Thailand": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Thailand" + } + }, + "nym:Kingdom of Tonga": { + "edges": { + "from": [ + "country:TO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of Tonga" + } + }, + "nym:Kingdom of the Netherlands": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kingdom of the Netherlands" + } + }, + "nym:Kingman Reef": { + "edges": { + "from": [ + "territory:UM-89" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kingman Reef" + } + }, + "nym:Kirghizia": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirghizia" + } + }, + "nym:Kirghizstan": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirghizstan" + } + }, + "nym:Kirgiz": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirgiz" + } + }, + "nym:Kirgizia": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirgizia" + } + }, + "nym:Kirgizija": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirgizija" + } + }, + "nym:Kirgizstan": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kirgizstan" + } + }, + "nym:Komori": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Komori" + } + }, + "nym:Kosova": { + "edges": { + "from": [ + "country:XK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kosova" + } + }, + "nym:Koweit": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Koweit" + } + }, + "nym:Kypros": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kypros" + } + }, + "nym:Kyrgyz Republic": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kyrgyz Republic" + } + }, + "nym:Kyrgyz Republickirghiz": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kyrgyz Republickirghiz" + } + }, + "nym:Kyrgyzstan": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Kyrgyzstan" + } + }, + "nym:KÃ˛rsou": { + "edges": { + "from": [ + "territory:CW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KÃ˛rsou" + } + }, + "nym:KÃļdÃļrÃļsÃĒse tÃŽ BÃĒafrÃŽka": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KÃļdÃļrÃļsÃĒse tÃŽ BÃĒafrÃŽka" + } + }, + "nym:KÃŊpros": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KÃŊpros" + } + }, + "nym:KÄąbrÄąs": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "KÄąbrÄąs" + } + }, + "nym:LA": { + "edges": { + "from": [ + "country:LA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LA" + } + }, + "nym:LB": { + "edges": { + "from": [ + "country:LB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LB" + } + }, + "nym:LC": { + "edges": { + "from": [ + "country:LC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LC" + } + }, + "nym:LI": { + "edges": { + "from": [ + "country:LI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LI" + } + }, + "nym:LK": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LK" + } + }, + "nym:LR": { + "edges": { + "from": [ + "country:LR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LR" + } + }, + "nym:LS": { + "edges": { + "from": [ + "country:LS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LS" + } + }, + "nym:LT": { + "edges": { + "from": [ + "country:LT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LT" + } + }, + "nym:LU": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LU" + } + }, + "nym:LV": { + "edges": { + "from": [ + "country:LV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LV" + } + }, + "nym:LY": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LY" + } + }, + "nym:Lao": { + "edges": { + "from": [ + "country:LA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Lao" + } + }, + "nym:Lao People's Democratic Republic": { + "edges": { + "from": [ + "country:LA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Lao People's Democratic Republic" + } + }, + "nym:Las Malvinas": { + "edges": { + "from": [ + "territory:FK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Las Malvinas" + } + }, + "nym:Latvija": { + "edges": { + "from": [ + "country:LV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Latvija" + } + }, + "nym:Latvijaz": { + "edges": { + "from": [ + "country:LV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Latvijaz" + } + }, + "nym:Lebanese Republic": { + "edges": { + "from": [ + "country:LB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Lebanese Republic" + } + }, + "nym:Libya": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Libya" + } + }, + "nym:Lietuva": { + "edges": { + "from": [ + "country:LT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Lietuva" + } + }, + "nym:Lubnān": { + "edges": { + "from": [ + "country:LB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Lubnān" + } + }, + "nym:Luxemborg": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Luxemborg" + } + }, + "nym:Luxemburg": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Luxemburg" + } + }, + "nym:LÃĢtzebuerg": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LÃĢtzebuerg" + } + }, + "nym:LÄĢbiyā": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "LÄĢbiyā" + } + }, + "nym:MA": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MA" + } + }, + "nym:MC": { + "edges": { + "from": [ + "country:MC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MC" + } + }, + "nym:MD": { + "edges": { + "from": [ + "country:MD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MD" + } + }, + "nym:ME": { + "edges": { + "from": [ + "country:ME" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ME" + } + }, + "nym:MF": { + "edges": { + "from": [ + "territory:MF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MF" + } + }, + "nym:MG": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MG" + } + }, + "nym:MH": { + "edges": { + "from": [ + "country:MH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MH" + } + }, + "nym:MK": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MK" + } + }, + "nym:ML": { + "edges": { + "from": [ + "country:ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ML" + } + }, + "nym:MM": { + "edges": { + "from": [ + "country:MM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MM" + } + }, + "nym:MN": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MN" + } + }, + "nym:MO": { + "edges": { + "from": [ + "territory:MO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MO" + } + }, + "nym:MP": { + "edges": { + "from": [ + "territory:MP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MP" + } + }, + "nym:MQ": { + "edges": { + "from": [ + "territory:MQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MQ" + } + }, + "nym:MR": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MR" + } + }, + "nym:MS": { + "edges": { + "from": [ + "territory:MS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MS" + } + }, + "nym:MT": { + "edges": { + "from": [ + "country:MT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MT" + } + }, + "nym:MU": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MU" + } + }, + "nym:MV": { + "edges": { + "from": [ + "country:MV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MV" + } + }, + "nym:MW": { + "edges": { + "from": [ + "country:MW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MW" + } + }, + "nym:MX": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MX" + } + }, + "nym:MY": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MY" + } + }, + "nym:MZ": { + "edges": { + "from": [ + "country:MZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MZ" + } + }, + "nym:Macao Special Administrative Region": { + "edges": { + "from": [ + "territory:MO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Macao Special Administrative Region" + } + }, + "nym:Macedon": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Macedon" + } + }, + "nym:Madagascar": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Madagascar" + } + }, + "nym:Madagasikara": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Madagasikara" + } + }, + "nym:Magyarorszag": { + "edges": { + "from": [ + "country:HU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Magyarorszag" + } + }, + "nym:MagyarorszÃĄg": { + "edges": { + "from": [ + "country:HU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MagyarorszÃĄg" + } + }, + "nym:Mainland china": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mainland china" + } + }, + "nym:Makedonija": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Makedonija" + } + }, + "nym:Malagasy Republic": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Malagasy Republic" + } + }, + "nym:Malaysia": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Malaysia" + } + }, + "nym:Malyasi": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Malyasi" + } + }, + "nym:MalÄ“ášŖiyā": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MalÄ“ášŖiyā" + } + }, + "nym:Maroc": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Maroc" + } + }, + "nym:Marruecos": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Marruecos" + } + }, + "nym:Martinique": { + "edges": { + "from": [ + "territory:MQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Martinique" + } + }, + "nym:Masr": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Masr" + } + }, + "nym:Maurice": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Maurice" + } + }, + "nym:Mauritanie": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mauritanie" + } + }, + "nym:Mauritius": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mauritius" + } + }, + "nym:Mayotte": { + "edges": { + "from": [ + "territory:YT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mayotte" + } + }, + "nym:Melilla": { + "edges": { + "from": [ + "territory:ES-ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Melilla" + } + }, + "nym:Mexcio": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mexcio" + } + }, + "nym:Mexicanos": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mexicanos" + } + }, + "nym:Mexixo": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mexixo" + } + }, + "nym:Midway Islands": { + "edges": { + "from": [ + "territory:UM-71" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Midway Islands" + } + }, + "nym:Misr": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Misr" + } + }, + "nym:Mocambique": { + "edges": { + "from": [ + "country:MZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mocambique" + } + }, + "nym:Moldavia": { + "edges": { + "from": [ + "country:MD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Moldavia" + } + }, + "nym:Mongol Uls": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mongol Uls" + } + }, + "nym:Mongolia": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mongolia" + } + }, + "nym:MongÎŗol Ulus": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MongÎŗol Ulus" + } + }, + "nym:MongÎŗol ulus": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MongÎŗol ulus" + } + }, + "nym:Montenegro": { + "edges": { + "from": [ + "country:ME" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Montenegro" + } + }, + "nym:Montserrat": { + "edges": { + "from": [ + "territory:MS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Montserrat" + } + }, + "nym:Moris": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Moris" + } + }, + "nym:Moçambique": { + "edges": { + "from": [ + "country:MZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Moçambique" + } + }, + "nym:Mueang Thai": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mueang Thai" + } + }, + "nym:Muritan": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Muritan" + } + }, + "nym:Muritaniya": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Muritaniya" + } + }, + "nym:Muscat and Oman": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Muscat and Oman" + } + }, + "nym:Myanma": { + "edges": { + "from": [ + "country:MM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Myanma" + } + }, + "nym:MÊxico": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MÊxico" + } + }, + "nym:Mēxihco": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Mēxihco" + } + }, + "nym:MÅĢrÄĢtānyā": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MÅĢrÄĢtānyā" + } + }, + "nym:MĮŽlÃĄixÄĢyà": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "MĮŽlÃĄixÄĢyà" + } + }, + "nym:NA": { + "edges": { + "from": [ + "country:NA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NA" + } + }, + "nym:NC": { + "edges": { + "from": [ + "territory:NC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NC" + } + }, + "nym:NE": { + "edges": { + "from": [ + "country:NE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NE" + } + }, + "nym:NF": { + "edges": { + "from": [ + "territory:NF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NF" + } + }, + "nym:NG": { + "edges": { + "from": [ + "country:NG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NG" + } + }, + "nym:NI": { + "edges": { + "from": [ + "country:NI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NI" + } + }, + "nym:NIR": { + "edges": { + "from": [ + "uk:NIR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NIR" + } + }, + "nym:NL": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NL" + } + }, + "nym:NO": { + "edges": { + "from": [ + "country:NO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NO" + } + }, + "nym:NP": { + "edges": { + "from": [ + "country:NP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NP" + } + }, + "nym:NR": { + "edges": { + "from": [ + "country:NR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NR" + } + }, + "nym:NU": { + "edges": { + "from": [ + "territory:NU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NU" + } + }, + "nym:NZ": { + "edges": { + "from": [ + "country:NZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NZ" + } + }, + "nym:Namhan": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Namhan" + } + }, + "nym:NamibiÃĢ": { + "edges": { + "from": [ + "country:NA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NamibiÃĢ" + } + }, + "nym:Naoero": { + "edges": { + "from": [ + "country:NR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Naoero" + } + }, + "nym:Navassa Island": { + "edges": { + "from": [ + "territory:UM-76" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Navassa Island" + } + }, + "nym:Naíjíríà": { + "edges": { + "from": [ + "country:NG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Naíjíríà" + } + }, + "nym:Nederland": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nederland" + } + }, + "nym:NederlÃĸn": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "NederlÃĸn" + } + }, + "nym:Nepāl": { + "edges": { + "from": [ + "country:NP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nepāl" + } + }, + "nym:New Caledonia": { + "edges": { + "from": [ + "territory:NC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "New Caledonia" + } + }, + "nym:New Hebrides": { + "edges": { + "from": [ + "country:VU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "New Hebrides" + } + }, + "nym:New Zealand": { + "edges": { + "from": [ + "country:NZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "New Zealand" + } + }, + "nym:Ngwane": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ngwane" + } + }, + "nym:Nihon": { + "edges": { + "from": [ + "country:JP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nihon" + } + }, + "nym:Nijar": { + "edges": { + "from": [ + "country:NE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nijar" + } + }, + "nym:Nijeriya": { + "edges": { + "from": [ + "country:NG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nijeriya" + } + }, + "nym:Nippon": { + "edges": { + "from": [ + "country:JP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nippon" + } + }, + "nym:Niue": { + "edges": { + "from": [ + "territory:NU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Niue" + } + }, + "nym:Niuē": { + "edges": { + "from": [ + "territory:NU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Niuē" + } + }, + "nym:Noreg": { + "edges": { + "from": [ + "country:NO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Noreg" + } + }, + "nym:Norge": { + "edges": { + "from": [ + "country:NO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Norge" + } + }, + "nym:North Korea": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "North Korea" + } + }, + "nym:Northern Ireland": { + "edges": { + "from": [ + "uk:NIR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Northern Ireland" + } + }, + "nym:Northern Marianas": { + "edges": { + "from": [ + "territory:MP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Northern Marianas" + } + }, + "nym:Northern Rhodesia": { + "edges": { + "from": [ + "country:ZM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Northern Rhodesia" + } + }, + "nym:Nouvelle-CalÊdonie": { + "edges": { + "from": [ + "territory:NC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nouvelle-CalÊdonie" + } + }, + "nym:Nyasaland": { + "edges": { + "from": [ + "country:MW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Nyasaland" + } + }, + "nym:O'zbekstan": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "O'zbekstan" + } + }, + "nym:OM": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "OM" + } + }, + "nym:Occupied Palestinian Territories": { + "edges": { + "from": [ + "territory:PS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Occupied Palestinian Territories" + } + }, + "nym:Oesterreich": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Oesterreich" + } + }, + "nym:Oriental Republic of Uruguay": { + "edges": { + "from": [ + "country:UY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Oriental Republic of Uruguay" + } + }, + "nym:Osterreich": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Osterreich" + } + }, + "nym:Outer Mongolia": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Outer Mongolia" + } + }, + "nym:O‘zbekiston": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "O‘zbekiston" + } + }, + "nym:O’zbekstan": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "O’zbekstan" + } + }, + "nym:PA": { + "edges": { + "from": [ + "country:PA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PA" + } + }, + "nym:PE": { + "edges": { + "from": [ + "country:PE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PE" + } + }, + "nym:PF": { + "edges": { + "from": [ + "territory:PF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PF" + } + }, + "nym:PG": { + "edges": { + "from": [ + "country:PG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PG" + } + }, + "nym:PH": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PH" + } + }, + "nym:PK": { + "edges": { + "from": [ + "country:PK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PK" + } + }, + "nym:PL": { + "edges": { + "from": [ + "country:PL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PL" + } + }, + "nym:PM": { + "edges": { + "from": [ + "territory:PM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PM" + } + }, + "nym:PN": { + "edges": { + "from": [ + "territory:PN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PN" + } + }, + "nym:PNG": { + "edges": { + "from": [ + "country:PG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PNG" + } + }, + "nym:PR": { + "edges": { + "from": [ + "territory:PR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PR" + } + }, + "nym:PRC": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PRC" + } + }, + "nym:PRK": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PRK" + } + }, + "nym:PS": { + "edges": { + "from": [ + "territory:PS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PS" + } + }, + "nym:PT": { + "edges": { + "from": [ + "country:PT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PT" + } + }, + "nym:PW": { + "edges": { + "from": [ + "country:PW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PW" + } + }, + "nym:PY": { + "edges": { + "from": [ + "country:PY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PY" + } + }, + "nym:Palmyra Atoll": { + "edges": { + "from": [ + "territory:UM-95" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Palmyra Atoll" + } + }, + "nym:PanamÃĄ": { + "edges": { + "from": [ + "country:PA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PanamÃĄ" + } + }, + "nym:Papua Niugini": { + "edges": { + "from": [ + "country:PG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Papua Niugini" + } + }, + "nym:ParaguÃĄi": { + "edges": { + "from": [ + "country:PY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ParaguÃĄi" + } + }, + "nym:Pelew": { + "edges": { + "from": [ + "country:PW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Pelew" + } + }, + "nym:People's Democratic Republic of Algeriaalgerie": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "People's Democratic Republic of Algeriaalgerie" + } + }, + "nym:People's Republic of Bangladesh": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "People's Republic of Bangladesh" + } + }, + "nym:People's Republic of China": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "People's Republic of China" + } + }, + "nym:Peoples Republic": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Peoples Republic" + } + }, + "nym:Persia": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Persia" + } + }, + "nym:PerÃē": { + "edges": { + "from": [ + "country:PE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PerÃē" + } + }, + "nym:Philippine Islands": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Philippine Islands" + } + }, + "nym:Phillippine": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Phillippine" + } + }, + "nym:Pilipinas": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Pilipinas" + } + }, + "nym:Pinas": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Pinas" + } + }, + "nym:Piruw": { + "edges": { + "from": [ + "country:PE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Piruw" + } + }, + "nym:Pitcairn": { + "edges": { + "from": [ + "territory:PN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Pitcairn" + } + }, + "nym:Pitcairn, Henderson, Ducie and Oeno Islands": { + "edges": { + "from": [ + "territory:PN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Pitcairn, Henderson, Ducie and Oeno Islands" + } + }, + "nym:Plurinational State of Bolivia": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Plurinational State of Bolivia" + } + }, + "nym:Polska": { + "edges": { + "from": [ + "country:PL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Polska" + } + }, + "nym:PolynÊsie Française": { + "edges": { + "from": [ + "territory:PF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PolynÊsie Française" + } + }, + "nym:PolynÊsie française": { + "edges": { + "from": [ + "territory:PF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "PolynÊsie française" + } + }, + "nym:Porto Rico": { + "edges": { + "from": [ + "territory:PR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Porto Rico" + } + }, + "nym:Portuguesa": { + "edges": { + "from": [ + "country:PT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Portuguesa" + } + }, + "nym:Portuguese Guinea": { + "edges": { + "from": [ + "country:GW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Portuguese Guinea" + } + }, + "nym:Portuguese Republic": { + "edges": { + "from": [ + "country:PT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Portuguese Republic" + } + }, + "nym:Prathet Thai": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Prathet Thai" + } + }, + "nym:Principality of Andorra": { + "edges": { + "from": [ + "country:AD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Principality of Andorra" + } + }, + "nym:Principality of Liechtenstein": { + "edges": { + "from": [ + "country:LI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Principality of Liechtenstein" + } + }, + "nym:Principality of Monaco": { + "edges": { + "from": [ + "country:MC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Principality of Monaco" + } + }, + "nym:Puarto Rico": { + "edges": { + "from": [ + "territory:PR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Puarto Rico" + } + }, + "nym:QA": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "QA" + } + }, + "nym:Qazaqstan": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Qazaqstan" + } + }, + "nym:RE": { + "edges": { + "from": [ + "territory:RE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RE" + } + }, + "nym:RO": { + "edges": { + "from": [ + "country:RO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RO" + } + }, + "nym:RS": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RS" + } + }, + "nym:RSA": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RSA" + } + }, + "nym:RSM": { + "edges": { + "from": [ + "country:SM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RSM" + } + }, + "nym:RU": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RU" + } + }, + "nym:RW": { + "edges": { + "from": [ + "country:RW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RW" + } + }, + "nym:Rastafari": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rastafari" + } + }, + "nym:Rastas": { + "edges": { + "from": [ + "country:JM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rastas" + } + }, + "nym:Ratcha-anachak Thai": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ratcha-anachak Thai" + } + }, + "nym:Repubblica": { + "edges": { + "from": [ + "country:SM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Repubblica" + } + }, + "nym:Repubilika ya Kongo": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Repubilika ya Kongo" + } + }, + "nym:Republic of Albania": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Albania" + } + }, + "nym:Republic of Angola": { + "edges": { + "from": [ + "country:AO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Angola" + } + }, + "nym:Republic of Armenia": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Armenia" + } + }, + "nym:Republic of Austria": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Austria" + } + }, + "nym:Republic of Azerbaijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Azerbaijan" + } + }, + "nym:Republic of Belarusbelorussia": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Belarusbelorussia" + } + }, + "nym:Republic of Benin": { + "edges": { + "from": [ + "country:BJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Benin" + } + }, + "nym:Republic of Bosnia and Herzegovina": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Bosnia and Herzegovina" + } + }, + "nym:Republic of Botswana": { + "edges": { + "from": [ + "country:BW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Botswana" + } + }, + "nym:Republic of Bulgaria": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Bulgaria" + } + }, + "nym:Republic of Burundi": { + "edges": { + "from": [ + "country:BI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Burundi" + } + }, + "nym:Republic of Cabo Verde": { + "edges": { + "from": [ + "country:CV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Cabo Verde" + } + }, + "nym:Republic of Cameroon": { + "edges": { + "from": [ + "country:CM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Cameroon" + } + }, + "nym:Republic of Chad": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Chad" + } + }, + "nym:Republic of Chile": { + "edges": { + "from": [ + "country:CL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Chile" + } + }, + "nym:Republic of Colombia": { + "edges": { + "from": [ + "country:CO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Colombia" + } + }, + "nym:Republic of Costa Rica": { + "edges": { + "from": [ + "country:CR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Costa Rica" + } + }, + "nym:Republic of Cote D'Ivoire": { + "edges": { + "from": [ + "country:CI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Cote D'Ivoire" + } + }, + "nym:Republic of Croatia": { + "edges": { + "from": [ + "country:HR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Croatia" + } + }, + "nym:Republic of Cuba": { + "edges": { + "from": [ + "country:CU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Cuba" + } + }, + "nym:Republic of Cyprus": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Cyprus" + } + }, + "nym:Republic of Djibouti": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Djibouti" + } + }, + "nym:Republic of Ecuador": { + "edges": { + "from": [ + "country:EC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Ecuador" + } + }, + "nym:Republic of El Salvador": { + "edges": { + "from": [ + "country:SV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of El Salvador" + } + }, + "nym:Republic of Equatorial Guinea": { + "edges": { + "from": [ + "country:GQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Equatorial Guinea" + } + }, + "nym:Republic of Estonia": { + "edges": { + "from": [ + "country:EE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Estonia" + } + }, + "nym:Republic of Fiji": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Fiji" + } + }, + "nym:Republic of Finland": { + "edges": { + "from": [ + "country:FI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Finland" + } + }, + "nym:Republic of Ghana": { + "edges": { + "from": [ + "country:GH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Ghana" + } + }, + "nym:Republic of Guatemala": { + "edges": { + "from": [ + "country:GT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Guatemala" + } + }, + "nym:Republic of Guinea": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Guinea" + } + }, + "nym:Republic of Guinea-Bissau": { + "edges": { + "from": [ + "country:GW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Guinea-Bissau" + } + }, + "nym:Republic of Haiti": { + "edges": { + "from": [ + "country:HT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Haiti" + } + }, + "nym:Republic of Honduras": { + "edges": { + "from": [ + "country:HN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Honduras" + } + }, + "nym:Republic of Iceland": { + "edges": { + "from": [ + "country:IS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Iceland" + } + }, + "nym:Republic of India": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of India" + } + }, + "nym:Republic of Indonesia": { + "edges": { + "from": [ + "country:ID" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Indonesia" + } + }, + "nym:Republic of Iraq": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Iraq" + } + }, + "nym:Republic of Ireland": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Ireland" + } + }, + "nym:Republic of Kazakhstankazak": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Kazakhstankazak" + } + }, + "nym:Republic of Kenya": { + "edges": { + "from": [ + "country:KE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Kenya" + } + }, + "nym:Republic of Kiribati": { + "edges": { + "from": [ + "country:KI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Kiribati" + } + }, + "nym:Republic of Korea": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Korea" + } + }, + "nym:Republic of Kosovo": { + "edges": { + "from": [ + "country:XK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Kosovo" + } + }, + "nym:Republic of Latvia": { + "edges": { + "from": [ + "country:LV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Latvia" + } + }, + "nym:Republic of Liberia": { + "edges": { + "from": [ + "country:LR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Liberia" + } + }, + "nym:Republic of Lithuanialietuva": { + "edges": { + "from": [ + "country:LT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Lithuanialietuva" + } + }, + "nym:Republic of Macedonia": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Macedonia" + } + }, + "nym:Republic of Madagascar": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Madagascar" + } + }, + "nym:Republic of Malawi": { + "edges": { + "from": [ + "country:MW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Malawi" + } + }, + "nym:Republic of Maldives": { + "edges": { + "from": [ + "country:MV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Maldives" + } + }, + "nym:Republic of Mali": { + "edges": { + "from": [ + "country:ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Mali" + } + }, + "nym:Republic of Malta": { + "edges": { + "from": [ + "country:MT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Malta" + } + }, + "nym:Republic of Mauritius": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Mauritius" + } + }, + "nym:Republic of Moldova": { + "edges": { + "from": [ + "country:MD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Moldova" + } + }, + "nym:Republic of Mozambique": { + "edges": { + "from": [ + "country:MZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Mozambique" + } + }, + "nym:Republic of Namibia": { + "edges": { + "from": [ + "country:NA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Namibia" + } + }, + "nym:Republic of Nauru": { + "edges": { + "from": [ + "country:NR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Nauru" + } + }, + "nym:Republic of Nicaragua": { + "edges": { + "from": [ + "country:NI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Nicaragua" + } + }, + "nym:Republic of Niger": { + "edges": { + "from": [ + "country:NE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Niger" + } + }, + "nym:Republic of Palau": { + "edges": { + "from": [ + "country:PW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Palau" + } + }, + "nym:Republic of Panama": { + "edges": { + "from": [ + "country:PA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Panama" + } + }, + "nym:Republic of Paraguay": { + "edges": { + "from": [ + "country:PY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Paraguay" + } + }, + "nym:Republic of Peru": { + "edges": { + "from": [ + "country:PE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Peru" + } + }, + "nym:Republic of Poland": { + "edges": { + "from": [ + "country:PL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Poland" + } + }, + "nym:Republic of Rwandaruanda": { + "edges": { + "from": [ + "country:RW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Rwandaruanda" + } + }, + "nym:Republic of San Marino": { + "edges": { + "from": [ + "country:SM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of San Marino" + } + }, + "nym:Republic of Senegal": { + "edges": { + "from": [ + "country:SN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Senegal" + } + }, + "nym:Republic of Serbia": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Serbia" + } + }, + "nym:Republic of Seychelles": { + "edges": { + "from": [ + "country:SC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Seychelles" + } + }, + "nym:Republic of Sierra Leone": { + "edges": { + "from": [ + "country:SL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Sierra Leone" + } + }, + "nym:Republic of Singapore": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Singapore" + } + }, + "nym:Republic of Slovenia": { + "edges": { + "from": [ + "country:SI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Slovenia" + } + }, + "nym:Republic of South Africa": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of South Africa" + } + }, + "nym:Republic of South Sudan": { + "edges": { + "from": [ + "country:SS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of South Sudan" + } + }, + "nym:Republic of Suriname": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Suriname" + } + }, + "nym:Republic of Tajikistantadjik": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Tajikistantadjik" + } + }, + "nym:Republic of Trinidad and Tobago": { + "edges": { + "from": [ + "country:TT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Trinidad and Tobago" + } + }, + "nym:Republic of Turkey": { + "edges": { + "from": [ + "country:TR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Turkey" + } + }, + "nym:Republic of Uganda": { + "edges": { + "from": [ + "country:UG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Uganda" + } + }, + "nym:Republic of Uzbekistan": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Uzbekistan" + } + }, + "nym:Republic of Vanuatu": { + "edges": { + "from": [ + "country:VU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Vanuatu" + } + }, + "nym:Republic of Yemen": { + "edges": { + "from": [ + "country:YE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Yemen" + } + }, + "nym:Republic of Zambia": { + "edges": { + "from": [ + "country:ZM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Zambia" + } + }, + "nym:Republic of Zimbabwe": { + "edges": { + "from": [ + "country:ZW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of Zimbabwe" + } + }, + "nym:Republic of the Congo": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of the Congo" + } + }, + "nym:Republic of the Marshall Islands": { + "edges": { + "from": [ + "country:MH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of the Marshall Islands" + } + }, + "nym:Republic of the Philippines": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of the Philippines" + } + }, + "nym:Republic of the Sudan": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of the Sudan" + } + }, + "nym:Republic of the Union of Myanmar": { + "edges": { + "from": [ + "country:MM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Republic of the Union of Myanmar" + } + }, + "nym:RepÃēblica Dominicana": { + "edges": { + "from": [ + "country:DO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RepÃēblica Dominicana" + } + }, + "nym:RepÃēblica Oriental del Uruguay": { + "edges": { + "from": [ + "country:UY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RepÃēblica Oriental del Uruguay" + } + }, + "nym:Reunion": { + "edges": { + "from": [ + "territory:RE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Reunion" + } + }, + "nym:Rgypt": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rgypt" + } + }, + "nym:Rhodesia": { + "edges": { + "from": [ + "country:ZW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rhodesia" + } + }, + "nym:Romania": { + "edges": { + "from": [ + "country:RO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Romania" + } + }, + "nym:RomÃĸnia": { + "edges": { + "from": [ + "country:RO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RomÃĸnia" + } + }, + "nym:Rossiya": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rossiya" + } + }, + "nym:RossiÃĸ": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RossiÃĸ" + } + }, + "nym:Roumania": { + "edges": { + "from": [ + "country:RO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Roumania" + } + }, + "nym:Rumania": { + "edges": { + "from": [ + "country:RO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rumania" + } + }, + "nym:Russian Federation": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Russian Federation" + } + }, + "nym:Rwandese Republic": { + "edges": { + "from": [ + "country:RW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Rwandese Republic" + } + }, + "nym:RÊpublique Centrafricaine": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique Centrafricaine" + } + }, + "nym:RÊpublique Française": { + "edges": { + "from": [ + "country:FR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique Française" + } + }, + "nym:RÊpublique Gabonaise": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique Gabonaise" + } + }, + "nym:RÊpublique du Congo": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique du Congo" + } + }, + "nym:RÊpublique dÊmocratique du Congo": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique dÊmocratique du Congo" + } + }, + "nym:RÊpublique française": { + "edges": { + "from": [ + "country:FR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique française" + } + }, + "nym:RÊpublique gabonaise": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÊpublique gabonaise" + } + }, + "nym:RÊunion": { + "edges": { + "from": [ + "territory:RE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "RÊunion" + } + }, + "nym:RÃŦoghachd Aonaichte": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "RÃŦoghachd Aonaichte" + } + }, + "nym:Ríocht Aontaithe": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ríocht Aontaithe" + } + }, + "nym:SA": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SA" + } + }, + "nym:SB": { + "edges": { + "from": [ + "country:SB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SB" + } + }, + "nym:SC": { + "edges": { + "from": [ + "country:SC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SC" + } + }, + "nym:SCT": { + "edges": { + "from": [ + "uk:SCT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SCT" + } + }, + "nym:SD": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SD" + } + }, + "nym:SE": { + "edges": { + "from": [ + "country:SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SE" + } + }, + "nym:SG": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SG" + } + }, + "nym:SH-AC": { + "edges": { + "from": [ + "territory:SH-AC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SH-AC" + } + }, + "nym:SH-HL": { + "edges": { + "from": [ + "territory:SH-HL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SH-HL" + } + }, + "nym:SH-TA": { + "edges": { + "from": [ + "territory:SH-TA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SH-TA" + } + }, + "nym:SI": { + "edges": { + "from": [ + "country:SI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SI" + } + }, + "nym:SJ": { + "edges": { + "from": [ + "territory:SJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SJ" + } + }, + "nym:SK": { + "edges": { + "from": [ + "country:SK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SK" + } + }, + "nym:SL": { + "edges": { + "from": [ + "country:SL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SL" + } + }, + "nym:SM": { + "edges": { + "from": [ + "country:SM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SM" + } + }, + "nym:SN": { + "edges": { + "from": [ + "country:SN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SN" + } + }, + "nym:SO": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SO" + } + }, + "nym:SR": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SR" + } + }, + "nym:SS": { + "edges": { + "from": [ + "country:SS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SS" + } + }, + "nym:ST": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ST" + } + }, + "nym:SU": { + "edges": { + "from": [ + "country:SU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SU" + } + }, + "nym:SV": { + "edges": { + "from": [ + "country:SV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SV" + } + }, + "nym:SX": { + "edges": { + "from": [ + "territory:SX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SX" + } + }, + "nym:SY": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SY" + } + }, + "nym:SZ": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SZ" + } + }, + "nym:Saba": { + "edges": { + "from": [ + "territory:BQ-SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saba" + } + }, + "nym:Saint BarthÊlemy": { + "edges": { + "from": [ + "territory:BL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint BarthÊlemy" + } + }, + "nym:Saint Helena": { + "edges": { + "from": [ + "territory:SH-HL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Helena" + } + }, + "nym:Saint Lucia": { + "edges": { + "from": [ + "country:LC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Lucia" + } + }, + "nym:Saint Pierre and Miquelon": { + "edges": { + "from": [ + "territory:PM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Pierre and Miquelon" + } + }, + "nym:Saint Vincent and the Grenadines": { + "edges": { + "from": [ + "country:VC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Vincent and the Grenadines" + } + }, + "nym:Saint-Pierre et Miquelon": { + "edges": { + "from": [ + "territory:PM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Saint-Pierre et Miquelon" + } + }, + "nym:Sak'art'velo": { + "edges": { + "from": [ + "country:GE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sak'art'velo" + } + }, + "nym:Sakartvelo": { + "edges": { + "from": [ + "country:GE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sakartvelo" + } + }, + "nym:Salvador": { + "edges": { + "from": [ + "country:SV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Salvador" + } + }, + "nym:Samo": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Samo" + } + }, + "nym:Samoa i Sisifo": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Samoa i Sisifo" + } + }, + "nym:Sao Thome e Principe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sao Thome e Principe" + } + }, + "nym:Sao Tome e Principe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sao Tome e Principe" + } + }, + "nym:Sarnam": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sarnam" + } + }, + "nym:Sarnam Sranangron": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sarnam Sranangron" + } + }, + "nym:Schweiz": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Schweiz" + } + }, + "nym:Scotland": { + "edges": { + "from": [ + "uk:SCT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Scotland" + } + }, + "nym:Scottland": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Scottland" + } + }, + "nym:Sesel": { + "edges": { + "from": [ + "country:SC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sesel" + } + }, + "nym:ShqipÃĢria": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ShqipÃĢria" + } + }, + "nym:Siam": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Siam" + } + }, + "nym:Singapur": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Singapur" + } + }, + "nym:Singapura": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Singapura" + } + }, + "nym:Sint Eustatius": { + "edges": { + "from": [ + "territory:BQ-SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sint Eustatius" + } + }, + "nym:Sint Maarten": { + "edges": { + "from": [ + "territory:SX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sint Maarten" + } + }, + "nym:Sion": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sion" + } + }, + "nym:Slovak Republic": { + "edges": { + "from": [ + "country:SK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Slovak Republic" + } + }, + "nym:Slovenija": { + "edges": { + "from": [ + "country:SI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Slovenija" + } + }, + "nym:Slovensko": { + "edges": { + "from": [ + "country:SK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Slovensko" + } + }, + "nym:SlovenskÃĄ": { + "edges": { + "from": [ + "country:SK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SlovenskÃĄ" + } + }, + "nym:Socialist Federal Republic of Yugoslavia": { + "edges": { + "from": [ + "country:YU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Socialist Federal Republic of Yugoslavia" + } + }, + "nym:Socialist People's Libyan Arab": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Socialist People's Libyan Arab" + } + }, + "nym:Socialist Republic of Vietnam": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Socialist Republic of Vietnam" + } + }, + "nym:Solomon Aelan": { + "edges": { + "from": [ + "country:SB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Solomon Aelan" + } + }, + "nym:Solomon Islands": { + "edges": { + "from": [ + "country:SB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Solomon Islands" + } + }, + "nym:Solomons": { + "edges": { + "from": [ + "country:SB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Solomons" + } + }, + "nym:Soomaaliya": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Soomaaliya" + } + }, + "nym:Soudan": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Soudan" + } + }, + "nym:South Georgia and the South Sandwich Islands": { + "edges": { + "from": [ + "territory:GS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "South Georgia and the South Sandwich Islands" + } + }, + "nym:South Korea": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "South Korea" + } + }, + "nym:South west africa": { + "edges": { + "from": [ + "country:NA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "South west africa" + } + }, + "nym:Sovereign Base Areas of Akrotiri and Dhekelia": { + "edges": { + "from": [ + "territory:XXD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sovereign Base Areas of Akrotiri and Dhekelia" + } + }, + "nym:Spanish Guinea": { + "edges": { + "from": [ + "country:GQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Spanish Guinea" + } + }, + "nym:Sranangron": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sranangron" + } + }, + "nym:Srbija": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Srbija" + } + }, + "nym:Sri Lankā": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sri Lankā" + } + }, + "nym:St Barth": { + "edges": { + "from": [ + "territory:BL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St Barth" + } + }, + "nym:St. Barthelemy": { + "edges": { + "from": [ + "territory:BL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St. Barthelemy" + } + }, + "nym:St. Kitts and Nevis": { + "edges": { + "from": [ + "country:KN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St. Kitts and Nevis" + } + }, + "nym:St. Lucia": { + "edges": { + "from": [ + "country:LC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St. Lucia" + } + }, + "nym:St. Thomas and Principe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St. Thomas and Principe" + } + }, + "nym:St. Vincent": { + "edges": { + "from": [ + "country:VC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "St. Vincent" + } + }, + "nym:State of Bahrain": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "State of Bahrain" + } + }, + "nym:State of Eritrea": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "State of Eritrea" + } + }, + "nym:State of Israel": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "State of Israel" + } + }, + "nym:State of Kuwait": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "State of Kuwait" + } + }, + "nym:State of Qatar": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "State of Qatar" + } + }, + "nym:Suid-Afrika": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Suid-Afrika" + } + }, + "nym:Suisse": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Suisse" + } + }, + "nym:Sultanate of Oman": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sultanate of Oman" + } + }, + "nym:Suomi": { + "edges": { + "from": [ + "country:FI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Suomi" + } + }, + "nym:Suriyah": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Suriyah" + } + }, + "nym:Svalbard and Jan Mayen": { + "edges": { + "from": [ + "territory:SJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Svalbard and Jan Mayen" + } + }, + "nym:Sverige": { + "edges": { + "from": [ + "country:SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Sverige" + } + }, + "nym:Svizra": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Svizra" + } + }, + "nym:Svizzera": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Svizzera" + } + }, + "nym:Swatini": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Swatini" + } + }, + "nym:Swiss Confederation": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Swiss Confederation" + } + }, + "nym:Switerland": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Switerland" + } + }, + "nym:Syria": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Syria" + } + }, + "nym:Syrian Arab Republic": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Syrian Arab Republic" + } + }, + "nym:SÃŖo TomÊ e Príncipe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SÃŖo TomÊ e Príncipe" + } + }, + "nym:SÊnÊgal": { + "edges": { + "from": [ + "country:SN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "SÊnÊgal" + } + }, + "nym:TC": { + "edges": { + "from": [ + "territory:TC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TC" + } + }, + "nym:TD": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TD" + } + }, + "nym:TF": { + "edges": { + "from": [ + "territory:TF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TF" + } + }, + "nym:TG": { + "edges": { + "from": [ + "country:TG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TG" + } + }, + "nym:TH": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TH" + } + }, + "nym:TJ": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TJ" + } + }, + "nym:TK": { + "edges": { + "from": [ + "territory:TK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TK" + } + }, + "nym:TL": { + "edges": { + "from": [ + "country:TL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TL" + } + }, + "nym:TM": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TM" + } + }, + "nym:TN": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TN" + } + }, + "nym:TO": { + "edges": { + "from": [ + "country:TO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TO" + } + }, + "nym:TR": { + "edges": { + "from": [ + "country:TR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TR" + } + }, + "nym:TT": { + "edges": { + "from": [ + "country:TT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TT" + } + }, + "nym:TV": { + "edges": { + "from": [ + "country:TV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TV" + } + }, + "nym:TW": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TW" + } + }, + "nym:TZ": { + "edges": { + "from": [ + "country:TZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TZ" + } + }, + "nym:Tadzhik": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tadzhik" + } + }, + "nym:Tadzhikistan": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tadzhikistan" + } + }, + "nym:Taiwan": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Taiwan" + } + }, + "nym:Tajik": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tajik" + } + }, + "nym:Tchad": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tchad" + } + }, + "nym:Territory of American Samoa": { + "edges": { + "from": [ + "territory:AS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of American Samoa" + } + }, + "nym:Territory of Christmas Island": { + "edges": { + "from": [ + "territory:CX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of Christmas Island" + } + }, + "nym:Territory of Guam": { + "edges": { + "from": [ + "territory:GU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of Guam" + } + }, + "nym:Territory of Heard Island and McDonald Islands": { + "edges": { + "from": [ + "territory:HM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of Heard Island and McDonald Islands" + } + }, + "nym:Territory of Norfolk Island": { + "edges": { + "from": [ + "territory:NF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of Norfolk Island" + } + }, + "nym:Territory of the Cocos (Keeling) Islands": { + "edges": { + "from": [ + "territory:CC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of the Cocos (Keeling) Islands" + } + }, + "nym:Territory of the Wallis and Futuna Islands": { + "edges": { + "from": [ + "territory:WF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Territory of the Wallis and Futuna Islands" + } + }, + "nym:The Arab Republic of Egypt": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Arab Republic of Egypt" + } + }, + "nym:The Argentine Republic": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Argentine Republic" + } + }, + "nym:The Bolivarian Republic of Venezuela": { + "edges": { + "from": [ + "country:VE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Bolivarian Republic of Venezuela" + } + }, + "nym:The British Indian Ocean Territory": { + "edges": { + "from": [ + "territory:IO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The British Indian Ocean Territory" + } + }, + "nym:The Central African Republic": { + "edges": { + "from": [ + "country:CF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Central African Republic" + } + }, + "nym:The Co-operative Republic of Guyana": { + "edges": { + "from": [ + "country:GY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Co-operative Republic of Guyana" + } + }, + "nym:The Commonwealth of Australia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Commonwealth of Australia" + } + }, + "nym:The Commonwealth of Dominica": { + "edges": { + "from": [ + "country:DM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Commonwealth of Dominica" + } + }, + "nym:The Commonwealth of The Bahamas": { + "edges": { + "from": [ + "country:BS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Commonwealth of The Bahamas" + } + }, + "nym:The Czech Republic": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Czech Republic" + } + }, + "nym:The Democratic People's Republic of Korea": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Democratic People's Republic of Korea" + } + }, + "nym:The Democratic Republic of Sao Tome and Principe": { + "edges": { + "from": [ + "country:ST" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Democratic Republic of Sao Tome and Principe" + } + }, + "nym:The Democratic Republic of Timor-Leste": { + "edges": { + "from": [ + "country:TL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Democratic Republic of Timor-Leste" + } + }, + "nym:The Democratic Republic of the Congo": { + "edges": { + "from": [ + "country:CD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Democratic Republic of the Congo" + } + }, + "nym:The Democratic Socialist Republic of Sri Lanka": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Democratic Socialist Republic of Sri Lanka" + } + }, + "nym:The Dominican Republic": { + "edges": { + "from": [ + "country:DO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Dominican Republic" + } + }, + "nym:The Federal Democratic Republic of Ethiopia": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federal Democratic Republic of Ethiopia" + } + }, + "nym:The Federal Democratic Republic of Nepal": { + "edges": { + "from": [ + "country:NP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federal Democratic Republic of Nepal" + } + }, + "nym:The Federal Republic of Germany": { + "edges": { + "from": [ + "country:DE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federal Republic of Germany" + } + }, + "nym:The Federal Republic of Nigeria": { + "edges": { + "from": [ + "country:NG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federal Republic of Nigeria" + } + }, + "nym:The Federated States of Micronesia": { + "edges": { + "from": [ + "country:FM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federated States of Micronesia" + } + }, + "nym:The Federation of Saint Christopher and Nevis": { + "edges": { + "from": [ + "country:KN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federation of Saint Christopher and Nevis" + } + }, + "nym:The Federative Republic of Brazil": { + "edges": { + "from": [ + "country:BR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Federative Republic of Brazil" + } + }, + "nym:The French Republic": { + "edges": { + "from": [ + "country:FR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The French Republic" + } + }, + "nym:The Gabonese Republic": { + "edges": { + "from": [ + "country:GA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Gabonese Republic" + } + }, + "nym:The Grand Duchy of Luxembourg": { + "edges": { + "from": [ + "country:LU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Grand Duchy of Luxembourg" + } + }, + "nym:The Hashemite Kingdom of Jordan": { + "edges": { + "from": [ + "country:JO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Hashemite Kingdom of Jordan" + } + }, + "nym:The Hellenic Republic": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Hellenic Republic" + } + }, + "nym:The Independent State of Papua New Guinea": { + "edges": { + "from": [ + "country:PG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Independent State of Papua New Guinea" + } + }, + "nym:The Independent State of Samoa": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Independent State of Samoa" + } + }, + "nym:The Islamic Republic of Afghanistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Islamic Republic of Afghanistan" + } + }, + "nym:The Islamic Republic of Iran": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Islamic Republic of Iran" + } + }, + "nym:The Islamic Republic of Mauritania": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Islamic Republic of Mauritania" + } + }, + "nym:The Islamic Republic of Pakistan": { + "edges": { + "from": [ + "country:PK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Islamic Republic of Pakistan" + } + }, + "nym:The Italian Republic": { + "edges": { + "from": [ + "country:IT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Italian Republic" + } + }, + "nym:The Kingdom of Bahrain": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Bahrain" + } + }, + "nym:The Kingdom of Belgium": { + "edges": { + "from": [ + "country:BE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Belgium" + } + }, + "nym:The Kingdom of Bhutan": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Bhutan" + } + }, + "nym:The Kingdom of Cambodia": { + "edges": { + "from": [ + "country:KH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Cambodia" + } + }, + "nym:The Kingdom of Denmark": { + "edges": { + "from": [ + "country:DK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Denmark" + } + }, + "nym:The Kingdom of Lesotho": { + "edges": { + "from": [ + "country:LS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Lesotho" + } + }, + "nym:The Kingdom of Morocco": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Morocco" + } + }, + "nym:The Kingdom of Norway": { + "edges": { + "from": [ + "country:NO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Norway" + } + }, + "nym:The Kingdom of Saudi Arabia": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Saudi Arabia" + } + }, + "nym:The Kingdom of Spain": { + "edges": { + "from": [ + "country:ES" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Spain" + } + }, + "nym:The Kingdom of Swaziland": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Swaziland" + } + }, + "nym:The Kingdom of Sweden": { + "edges": { + "from": [ + "country:SE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Sweden" + } + }, + "nym:The Kingdom of Thailand": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Thailand" + } + }, + "nym:The Kingdom of Tonga": { + "edges": { + "from": [ + "country:TO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of Tonga" + } + }, + "nym:The Kingdom of the Netherlands": { + "edges": { + "from": [ + "country:NL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kingdom of the Netherlands" + } + }, + "nym:The Kyrgyz Republic": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Kyrgyz Republic" + } + }, + "nym:The Lao People's Democratic Republic": { + "edges": { + "from": [ + "country:LA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Lao People's Democratic Republic" + } + }, + "nym:The Lebanese Republic": { + "edges": { + "from": [ + "country:LB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Lebanese Republic" + } + }, + "nym:The Occupied Palestinian Territories": { + "edges": { + "from": [ + "territory:PS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Occupied Palestinian Territories" + } + }, + "nym:The Oriental Republic of Uruguay": { + "edges": { + "from": [ + "country:UY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Oriental Republic of Uruguay" + } + }, + "nym:The People's Democratic Republic of Algeria": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The People's Democratic Republic of Algeria" + } + }, + "nym:The People's Republic of Bangladesh": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The People's Republic of Bangladesh" + } + }, + "nym:The People's Republic of China": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The People's Republic of China" + } + }, + "nym:The Plurinational State of Bolivia": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Plurinational State of Bolivia" + } + }, + "nym:The Portuguese Republic": { + "edges": { + "from": [ + "country:PT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Portuguese Republic" + } + }, + "nym:The Principality of Andorra": { + "edges": { + "from": [ + "country:AD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Principality of Andorra" + } + }, + "nym:The Principality of Liechtenstein": { + "edges": { + "from": [ + "country:LI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Principality of Liechtenstein" + } + }, + "nym:The Principality of Monaco": { + "edges": { + "from": [ + "country:MC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Principality of Monaco" + } + }, + "nym:The Republic of Albania": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Albania" + } + }, + "nym:The Republic of Angola": { + "edges": { + "from": [ + "country:AO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Angola" + } + }, + "nym:The Republic of Armenia": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Armenia" + } + }, + "nym:The Republic of Austria": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Austria" + } + }, + "nym:The Republic of Azerbaijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Azerbaijan" + } + }, + "nym:The Republic of Belarus": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Belarus" + } + }, + "nym:The Republic of Benin": { + "edges": { + "from": [ + "country:BJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Benin" + } + }, + "nym:The Republic of Botswana": { + "edges": { + "from": [ + "country:BW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Botswana" + } + }, + "nym:The Republic of Bulgaria": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Bulgaria" + } + }, + "nym:The Republic of Burundi": { + "edges": { + "from": [ + "country:BI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Burundi" + } + }, + "nym:The Republic of Cabo Verde": { + "edges": { + "from": [ + "country:CV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Cabo Verde" + } + }, + "nym:The Republic of Cameroon": { + "edges": { + "from": [ + "country:CM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Cameroon" + } + }, + "nym:The Republic of Chad": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Chad" + } + }, + "nym:The Republic of Chile": { + "edges": { + "from": [ + "country:CL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Chile" + } + }, + "nym:The Republic of Colombia": { + "edges": { + "from": [ + "country:CO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Colombia" + } + }, + "nym:The Republic of Costa Rica": { + "edges": { + "from": [ + "country:CR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Costa Rica" + } + }, + "nym:The Republic of Cote D'Ivoire": { + "edges": { + "from": [ + "country:CI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Cote D'Ivoire" + } + }, + "nym:The Republic of Croatia": { + "edges": { + "from": [ + "country:HR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Croatia" + } + }, + "nym:The Republic of Cuba": { + "edges": { + "from": [ + "country:CU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Cuba" + } + }, + "nym:The Republic of Cyprus": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Cyprus" + } + }, + "nym:The Republic of Djibouti": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Djibouti" + } + }, + "nym:The Republic of Ecuador": { + "edges": { + "from": [ + "country:EC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Ecuador" + } + }, + "nym:The Republic of El Salvador": { + "edges": { + "from": [ + "country:SV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of El Salvador" + } + }, + "nym:The Republic of Equatorial Guinea": { + "edges": { + "from": [ + "country:GQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Equatorial Guinea" + } + }, + "nym:The Republic of Estonia": { + "edges": { + "from": [ + "country:EE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Estonia" + } + }, + "nym:The Republic of Fiji": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Fiji" + } + }, + "nym:The Republic of Finland": { + "edges": { + "from": [ + "country:FI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Finland" + } + }, + "nym:The Republic of Ghana": { + "edges": { + "from": [ + "country:GH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Ghana" + } + }, + "nym:The Republic of Guatemala": { + "edges": { + "from": [ + "country:GT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Guatemala" + } + }, + "nym:The Republic of Guinea": { + "edges": { + "from": [ + "country:GN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Guinea" + } + }, + "nym:The Republic of Guinea-Bissau": { + "edges": { + "from": [ + "country:GW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Guinea-Bissau" + } + }, + "nym:The Republic of Haiti": { + "edges": { + "from": [ + "country:HT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Haiti" + } + }, + "nym:The Republic of Honduras": { + "edges": { + "from": [ + "country:HN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Honduras" + } + }, + "nym:The Republic of Iceland": { + "edges": { + "from": [ + "country:IS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Iceland" + } + }, + "nym:The Republic of India": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of India" + } + }, + "nym:The Republic of Indonesia": { + "edges": { + "from": [ + "country:ID" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Indonesia" + } + }, + "nym:The Republic of Iraq": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Iraq" + } + }, + "nym:The Republic of Kazakhstan": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Kazakhstan" + } + }, + "nym:The Republic of Kenya": { + "edges": { + "from": [ + "country:KE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Kenya" + } + }, + "nym:The Republic of Kiribati": { + "edges": { + "from": [ + "country:KI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Kiribati" + } + }, + "nym:The Republic of Korea": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Korea" + } + }, + "nym:The Republic of Kosovo": { + "edges": { + "from": [ + "country:XK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Kosovo" + } + }, + "nym:The Republic of Latvia": { + "edges": { + "from": [ + "country:LV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Latvia" + } + }, + "nym:The Republic of Liberia": { + "edges": { + "from": [ + "country:LR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Liberia" + } + }, + "nym:The Republic of Lithuania": { + "edges": { + "from": [ + "country:LT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Lithuania" + } + }, + "nym:The Republic of Macedonia": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Macedonia" + } + }, + "nym:The Republic of Madagascar": { + "edges": { + "from": [ + "country:MG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Madagascar" + } + }, + "nym:The Republic of Malawi": { + "edges": { + "from": [ + "country:MW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Malawi" + } + }, + "nym:The Republic of Maldives": { + "edges": { + "from": [ + "country:MV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Maldives" + } + }, + "nym:The Republic of Mali": { + "edges": { + "from": [ + "country:ML" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Mali" + } + }, + "nym:The Republic of Malta": { + "edges": { + "from": [ + "country:MT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Malta" + } + }, + "nym:The Republic of Mauritius": { + "edges": { + "from": [ + "country:MU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Mauritius" + } + }, + "nym:The Republic of Moldova": { + "edges": { + "from": [ + "country:MD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Moldova" + } + }, + "nym:The Republic of Mozambique": { + "edges": { + "from": [ + "country:MZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Mozambique" + } + }, + "nym:The Republic of Namibia": { + "edges": { + "from": [ + "country:NA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Namibia" + } + }, + "nym:The Republic of Nauru": { + "edges": { + "from": [ + "country:NR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Nauru" + } + }, + "nym:The Republic of Nicaragua": { + "edges": { + "from": [ + "country:NI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Nicaragua" + } + }, + "nym:The Republic of Niger": { + "edges": { + "from": [ + "country:NE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Niger" + } + }, + "nym:The Republic of Palau": { + "edges": { + "from": [ + "country:PW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Palau" + } + }, + "nym:The Republic of Panama": { + "edges": { + "from": [ + "country:PA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Panama" + } + }, + "nym:The Republic of Paraguay": { + "edges": { + "from": [ + "country:PY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Paraguay" + } + }, + "nym:The Republic of Peru": { + "edges": { + "from": [ + "country:PE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Peru" + } + }, + "nym:The Republic of Poland": { + "edges": { + "from": [ + "country:PL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Poland" + } + }, + "nym:The Republic of Rwanda": { + "edges": { + "from": [ + "country:RW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Rwanda" + } + }, + "nym:The Republic of San Marino": { + "edges": { + "from": [ + "country:SM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of San Marino" + } + }, + "nym:The Republic of Senegal": { + "edges": { + "from": [ + "country:SN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Senegal" + } + }, + "nym:The Republic of Serbia": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Serbia" + } + }, + "nym:The Republic of Seychelles": { + "edges": { + "from": [ + "country:SC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Seychelles" + } + }, + "nym:The Republic of Sierra Leone": { + "edges": { + "from": [ + "country:SL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Sierra Leone" + } + }, + "nym:The Republic of Singapore": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Singapore" + } + }, + "nym:The Republic of Slovenia": { + "edges": { + "from": [ + "country:SI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Slovenia" + } + }, + "nym:The Republic of South Africa": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of South Africa" + } + }, + "nym:The Republic of South Sudan": { + "edges": { + "from": [ + "country:SS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of South Sudan" + } + }, + "nym:The Republic of Suriname": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Suriname" + } + }, + "nym:The Republic of Tajikistan": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Tajikistan" + } + }, + "nym:The Republic of The Gambia": { + "edges": { + "from": [ + "country:GM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of The Gambia" + } + }, + "nym:The Republic of Trinidad and Tobago": { + "edges": { + "from": [ + "country:TT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Trinidad and Tobago" + } + }, + "nym:The Republic of Turkey": { + "edges": { + "from": [ + "country:TR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Turkey" + } + }, + "nym:The Republic of Uganda": { + "edges": { + "from": [ + "country:UG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Uganda" + } + }, + "nym:The Republic of Uzbekistan": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Uzbekistan" + } + }, + "nym:The Republic of Vanuatu": { + "edges": { + "from": [ + "country:VU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Vanuatu" + } + }, + "nym:The Republic of Yemen": { + "edges": { + "from": [ + "country:YE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Yemen" + } + }, + "nym:The Republic of Zambia": { + "edges": { + "from": [ + "country:ZM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Zambia" + } + }, + "nym:The Republic of Zimbabwe": { + "edges": { + "from": [ + "country:ZW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of Zimbabwe" + } + }, + "nym:The Republic of the Congo": { + "edges": { + "from": [ + "country:CG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of the Congo" + } + }, + "nym:The Republic of the Marshall Islands": { + "edges": { + "from": [ + "country:MH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of the Marshall Islands" + } + }, + "nym:The Republic of the Philippines": { + "edges": { + "from": [ + "country:PH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of the Philippines" + } + }, + "nym:The Republic of the Sudan": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of the Sudan" + } + }, + "nym:The Republic of the Union of Myanmar": { + "edges": { + "from": [ + "country:MM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Republic of the Union of Myanmar" + } + }, + "nym:The Russian Federation": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Russian Federation" + } + }, + "nym:The Slovak Republic": { + "edges": { + "from": [ + "country:SK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Slovak Republic" + } + }, + "nym:The Socialist Republic of Vietnam": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Socialist Republic of Vietnam" + } + }, + "nym:The State of Eritrea": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The State of Eritrea" + } + }, + "nym:The State of Israel": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The State of Israel" + } + }, + "nym:The State of Kuwait": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The State of Kuwait" + } + }, + "nym:The State of Qatar": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The State of Qatar" + } + }, + "nym:The States": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "The States" + } + }, + "nym:The Sultanate of Oman": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Sultanate of Oman" + } + }, + "nym:The Swiss Confederation": { + "edges": { + "from": [ + "country:CH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Swiss Confederation" + } + }, + "nym:The Syrian Arab Republic": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Syrian Arab Republic" + } + }, + "nym:The Togolese Republic": { + "edges": { + "from": [ + "country:TG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Togolese Republic" + } + }, + "nym:The Tunisian Republic": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Tunisian Republic" + } + }, + "nym:The Union of the Comoros": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Union of the Comoros" + } + }, + "nym:The United Arab Emirates": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The United Arab Emirates" + } + }, + "nym:The United Kingdom of Great Britain and Northern Ireland": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The United Kingdom of Great Britain and Northern Ireland" + } + }, + "nym:The United Mexican States": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The United Mexican States" + } + }, + "nym:The United Republic of Tanzania": { + "edges": { + "from": [ + "country:TZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The United Republic of Tanzania" + } + }, + "nym:The United States of America": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The United States of America" + } + }, + "nym:The Virgin Islands": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "The Virgin Islands" + } + }, + "nym:Timor Lorosa'e": { + "edges": { + "from": [ + "country:TL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Timor Lorosa'e" + } + }, + "nym:Timor-Leste": { + "edges": { + "from": [ + "country:TL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Timor-Leste" + } + }, + "nym:Togolese": { + "edges": { + "from": [ + "country:TG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Togolese" + } + }, + "nym:Togolese Republic": { + "edges": { + "from": [ + "country:TG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Togolese Republic" + } + }, + "nym:Tojikistan": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tojikistan" + } + }, + "nym:Tokelau": { + "edges": { + "from": [ + "territory:TK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tokelau" + } + }, + "nym:Toçikiston": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Toçikiston" + } + }, + "nym:Tristan da Cunha": { + "edges": { + "from": [ + "territory:SH-TA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tristan da Cunha" + } + }, + "nym:Tunes": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tunes" + } + }, + "nym:Tunisian Republic": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Tunisian Republic" + } + }, + "nym:Turkiye": { + "edges": { + "from": [ + "country:TR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Turkiye" + } + }, + "nym:Turkmen": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Turkmen" + } + }, + "nym:Turkmenia": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Turkmenia" + } + }, + "nym:Turkmenistan": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Turkmenistan" + } + }, + "nym:Turkomen": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Turkomen" + } + }, + "nym:Turks and Caicos Islands": { + "edges": { + "from": [ + "territory:TC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Turks and Caicos Islands" + } + }, + "nym:Tuvalu": { + "edges": { + "from": [ + "country:TV" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tuvalu" + } + }, + "nym:TÃĄiwān": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TÃĄiwān" + } + }, + "nym:TÃŧrkiye": { + "edges": { + "from": [ + "country:TR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TÃŧrkiye" + } + }, + "nym:TÃŧrkmenistan": { + "edges": { + "from": [ + "country:TM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TÃŧrkmenistan" + } + }, + "nym:TÅĄÄd": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TÅĄÄd" + } + }, + "nym:TÅĢns": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "TÅĢns" + } + }, + "nym:U.K.": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "U.K." + } + }, + "nym:U.S.": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "U.S." + } + }, + "nym:U.S.A.": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "U.S.A." + } + }, + "nym:UA": { + "edges": { + "from": [ + "country:UA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UA" + } + }, + "nym:UAE": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UAE" + } + }, + "nym:UG": { + "edges": { + "from": [ + "country:UG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UG" + } + }, + "nym:UK": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UK" + } + }, + "nym:UM-67": { + "edges": { + "from": [ + "territory:UM-67" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-67" + } + }, + "nym:UM-71": { + "edges": { + "from": [ + "territory:UM-71" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-71" + } + }, + "nym:UM-76": { + "edges": { + "from": [ + "territory:UM-76" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-76" + } + }, + "nym:UM-81": { + "edges": { + "from": [ + "territory:UM-81" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-81" + } + }, + "nym:UM-84": { + "edges": { + "from": [ + "territory:UM-84" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-84" + } + }, + "nym:UM-86": { + "edges": { + "from": [ + "territory:UM-86" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-86" + } + }, + "nym:UM-89": { + "edges": { + "from": [ + "territory:UM-89" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-89" + } + }, + "nym:UM-95": { + "edges": { + "from": [ + "territory:UM-95" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UM-95" + } + }, + "nym:US": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "US" + } + }, + "nym:USA": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "USA" + } + }, + "nym:UY": { + "edges": { + "from": [ + "country:UY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UY" + } + }, + "nym:UZ": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UZ" + } + }, + "nym:Ukraine": { + "edges": { + "from": [ + "country:UA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ukraine" + } + }, + "nym:Ukrayina": { + "edges": { + "from": [ + "country:UA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ukrayina" + } + }, + "nym:UkraŅ—na": { + "edges": { + "from": [ + "country:UA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "UkraŅ—na" + } + }, + "nym:Umbuso weSwatini": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Umbuso weSwatini" + } + }, + "nym:Union of Soviet Socialist Republics": { + "edges": { + "from": [ + "country:SU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Union of Soviet Socialist Republics" + } + }, + "nym:Union of the Comoros": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Union of the Comoros" + } + }, + "nym:Unit States": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Unit States" + } + }, + "nym:Unite States": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Unite States" + } + }, + "nym:United Arab Emirates": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Arab Emirates" + } + }, + "nym:United Arab Republic": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Arab Republic" + } + }, + "nym:United Kingdom of Great Britain and Northern Ireland": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Kingdom of Great Britain and Northern Ireland" + } + }, + "nym:United Mexican States": { + "edges": { + "from": [ + "country:MX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Mexican States" + } + }, + "nym:United Republic of Tanzania": { + "edges": { + "from": [ + "country:TZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Republic of Tanzania" + } + }, + "nym:United Sat": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Sat" + } + }, + "nym:United Staes": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Staes" + } + }, + "nym:United Stated": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Stated" + } + }, + "nym:United States of America": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United States of America" + } + }, + "nym:United Stats": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Stats" + } + }, + "nym:United Sttes": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "United Sttes" + } + }, + "nym:Unites States": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Unites States" + } + }, + "nym:Unitit Kinrick": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Unitit Kinrick" + } + }, + "nym:Untied State": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Untied State" + } + }, + "nym:Uvea mo Futuna": { + "edges": { + "from": [ + "territory:WF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Uvea mo Futuna" + } + }, + "nym:Uzbek": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Uzbek" + } + }, + "nym:VA": { + "edges": { + "from": [ + "country:VA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VA" + } + }, + "nym:VC": { + "edges": { + "from": [ + "country:VC" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VC" + } + }, + "nym:VE": { + "edges": { + "from": [ + "country:VE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VE" + } + }, + "nym:VG": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VG" + } + }, + "nym:VI": { + "edges": { + "from": [ + "territory:VI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VI" + } + }, + "nym:VN": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VN" + } + }, + "nym:VU": { + "edges": { + "from": [ + "country:VU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "VU" + } + }, + "nym:Vatican City State": { + "edges": { + "from": [ + "country:VA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Vatican City State" + } + }, + "nym:Veitnam": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Veitnam" + } + }, + "nym:Venezuela": { + "edges": { + "from": [ + "country:VE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Venezuela" + } + }, + "nym:Venezula": { + "edges": { + "from": [ + "country:VE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Venezula" + } + }, + "nym:Vietman": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Vietman" + } + }, + "nym:Virgin Islands": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Virgin Islands" + } + }, + "nym:Virgin Islands of the United States": { + "edges": { + "from": [ + "territory:VI" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Virgin Islands of the United States" + } + }, + "nym:Virgina Islands": { + "edges": { + "from": [ + "territory:VG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Virgina Islands" + } + }, + "nym:Viti": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Viti" + } + }, + "nym:Viáģ‡t Nam": { + "edges": { + "from": [ + "country:VN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Viáģ‡t Nam" + } + }, + "nym:Volta": { + "edges": { + "from": [ + "country:BF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Volta" + } + }, + "nym:Volívia": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Volívia" + } + }, + "nym:WF": { + "edges": { + "from": [ + "territory:WF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "WF" + } + }, + "nym:WLS": { + "edges": { + "from": [ + "uk:WLS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "WLS" + } + }, + "nym:WS": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "WS" + } + }, + "nym:Wales": { + "edges": { + "from": [ + "uk:WLS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Wales" + } + }, + "nym:Wallis-et-Futuna": { + "edges": { + "from": [ + "territory:WF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Wallis-et-Futuna" + } + }, + "nym:West Pakistan": { + "edges": { + "from": [ + "country:PK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "West Pakistan" + } + }, + "nym:Western Sahara": { + "edges": { + "from": [ + "territory:EH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Western Sahara" + } + }, + "nym:Western samoa": { + "edges": { + "from": [ + "country:WS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Western samoa" + } + }, + "nym:White Russia": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "White Russia" + } + }, + "nym:Wuliwya": { + "edges": { + "from": [ + "country:BO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Wuliwya" + } + }, + "nym:XK": { + "edges": { + "from": [ + "country:XK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "XK" + } + }, + "nym:XQZ": { + "edges": { + "from": [ + "territory:XQZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "XQZ" + } + }, + "nym:XXD": { + "edges": { + "from": [ + "territory:XXD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "XXD" + } + }, + "nym:XÄĢnjiāpō": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "XÄĢnjiāpō" + } + }, + "nym:Y Deyrnas Unedig": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Y Deyrnas Unedig" + } + }, + "nym:YE": { + "edges": { + "from": [ + "country:YE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "YE" + } + }, + "nym:YT": { + "edges": { + "from": [ + "territory:YT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "YT" + } + }, + "nym:YU": { + "edges": { + "from": [ + "country:YU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "YU" + } + }, + "nym:Yaltopya": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Yaltopya" + } + }, + "nym:Yisra'el": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Yisra'el" + } + }, + "nym:Yisrael": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Yisrael" + } + }, + "nym:Yugosav": { + "edges": { + "from": [ + "country:YU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Yugosav" + } + }, + "nym:ZA": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ZA" + } + }, + "nym:ZM": { + "edges": { + "from": [ + "country:ZM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ZM" + } + }, + "nym:ZW": { + "edges": { + "from": [ + "country:ZW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ZW" + } + }, + "nym:Zealnd": { + "edges": { + "from": [ + "country:NZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zealnd" + } + }, + "nym:Zeland": { + "edges": { + "from": [ + "country:NZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zeland" + } + }, + "nym:Zhongguo": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zhongguo" + } + }, + "nym:Zhonghua": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zhonghua" + } + }, + "nym:Zhonghua Peoples Republic": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zhonghua Peoples Republic" + } + }, + "nym:ZhōngguÃŗ": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ZhōngguÃŗ" + } + }, + "nym:ZhōnghuÃĄ MínguÃŗ": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ZhōnghuÃĄ MínguÃŗ" + } + }, + "nym:Zion": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Zion" + } + }, + "nym:Ztate of Katar": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ztate of Katar" + } + }, + "nym:afganastan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afganastan" + } + }, + "nym:afganestan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afganestan" + } + }, + "nym:afganhistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afganhistan" + } + }, + "nym:afganistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afganistan" + } + }, + "nym:afghanistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afghanistan" + } + }, + "nym:afghanlstan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afghanlstan" + } + }, + "nym:afghistan": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "afghistan" + } + }, + "nym:aigeria": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "aigeria" + } + }, + "nym:alabnia": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "alabnia" + } + }, + "nym:albana": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albana" + } + }, + "nym:albania": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albania" + } + }, + "nym:albanian": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albanian" + } + }, + "nym:albanija": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albanija" + } + }, + "nym:albenia": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albenia" + } + }, + "nym:albiana": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "albiana" + } + }, + "nym:alegeria": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "alegeria" + } + }, + "nym:algeir": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algeir" + } + }, + "nym:algeirs": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algeirs" + } + }, + "nym:algeria": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algeria" + } + }, + "nym:algers": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algers" + } + }, + "nym:algieria": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algieria" + } + }, + "nym:algiers": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "algiers" + } + }, + "nym:alibania": { + "edges": { + "from": [ + "country:AL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "alibania" + } + }, + "nym:americia": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "americia" + } + }, + "nym:angolo": { + "edges": { + "from": [ + "country:AO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "angolo" + } + }, + "nym:arab emir ates": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "arab emir ates" + } + }, + "nym:arab emirates": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "arab emirates" + } + }, + "nym:argentiha": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "argentiha" + } + }, + "nym:argentina": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "argentina" + } + }, + "nym:argentine": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "argentine" + } + }, + "nym:argentna": { + "edges": { + "from": [ + "country:AR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "argentna" + } + }, + "nym:arima": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "arima" + } + }, + "nym:armenia": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "armenia" + } + }, + "nym:arminia": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "arminia" + } + }, + "nym:ausralia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ausralia" + } + }, + "nym:austalia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "austalia" + } + }, + "nym:austraila": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "austraila" + } + }, + "nym:austrailia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "austrailia" + } + }, + "nym:australa": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "australa" + } + }, + "nym:australla": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "australla" + } + }, + "nym:austrilia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "austrilia" + } + }, + "nym:austrlia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "austrlia" + } + }, + "nym:autralia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "autralia" + } + }, + "nym:avstralia": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "avstralia" + } + }, + "nym:avstria": { + "edges": { + "from": [ + "country:AU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "avstria" + } + }, + "nym:azebaijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azebaijan" + } + }, + "nym:azeraijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azeraijan" + } + }, + "nym:azerbaijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbaijan" + } + }, + "nym:azerbaijann": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbaijann" + } + }, + "nym:azerbaisan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbaisan" + } + }, + "nym:azerbaizan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbaizan" + } + }, + "nym:azerbajan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbajan" + } + }, + "nym:azerbaycan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbaycan" + } + }, + "nym:azerbayjan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbayjan" + } + }, + "nym:azerbeyjan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbeyjan" + } + }, + "nym:azerbpijan": { + "edges": { + "from": [ + "country:AZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "azerbpijan" + } + }, + "nym:aş-ŞÅĢmāl": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "aş-ŞÅĢmāl" + } + }, + "nym:iNingizimu Afrika": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "iNingizimu Afrika" + } + }, + "nym:iSewula Afrika": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "iSewula Afrika" + } + }, + "nym:il-ikwet": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "il-ikwet" + } + }, + "nym:kaNgwane": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "kaNgwane" + } + }, + "nym:uMzantsi Afrika": { + "edges": { + "from": [ + "country:ZA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "uMzantsi Afrika" + } + }, + "nym:weSwatini": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "weSwatini" + } + }, + "nym:weSwatini Swatini Ngwane": { + "edges": { + "from": [ + "country:SZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "weSwatini Swatini Ngwane" + } + }, + "nym:Åland": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Åland" + } + }, + "nym:Åland Islands": { + "edges": { + "from": [ + "territory:AX" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Åland Islands" + } + }, + "nym:Éire": { + "edges": { + "from": [ + "country:IE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Éire" + } + }, + "nym:États-Unis": { + "edges": { + "from": [ + "country:US" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "États-Unis" + } + }, + "nym:Ísland": { + "edges": { + "from": [ + "country:IS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ísland" + } + }, + "nym:Îraq": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Îraq" + } + }, + "nym:Österreich": { + "edges": { + "from": [ + "country:AT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Österreich" + } + }, + "nym:Česko": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Česko" + } + }, + "nym:ČeskÃĄ": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ČeskÃĄ" + } + }, + "nym:ČeskÃĄ republika": { + "edges": { + "from": [ + "country:CZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ČeskÃĄ republika" + } + }, + "nym:ÄĒrān": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ÄĒrān" + } + }, + "nym:ΕÎģÎģÎŦδι": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ΕÎģÎģÎŦδι" + } + }, + "nym:ΕÎģÎģÎŦĪ‚": { + "edges": { + "from": [ + "country:GR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ΕÎģÎģÎŦĪ‚" + } + }, + "nym:ÎšĪĪ€ĪÎŋĪ‚": { + "edges": { + "from": [ + "country:CY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ÎšĪĪ€ĪÎŋĪ‚" + } + }, + "nym:ЎзбĐĩĐēĐ¸ŅŅ‚ĐžĐŊ": { + "edges": { + "from": [ + "country:UZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ЎзбĐĩĐēĐ¸ŅŅ‚ĐžĐŊ" + } + }, + "nym:БĐĩĐģĐ°Ņ€ŅƒŅŅŒ": { + "edges": { + "from": [ + "country:BY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "БĐĩĐģĐ°Ņ€ŅƒŅŅŒ" + } + }, + "nym:Đ‘ĐžŅĐŊа и ĐĨĐĩҀ҆ĐĩĐŗĐžĐ˛Đ¸ĐŊа": { + "edges": { + "from": [ + "country:BA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Đ‘ĐžŅĐŊа и ĐĨĐĩҀ҆ĐĩĐŗĐžĐ˛Đ¸ĐŊа" + } + }, + "nym:Đ‘ŅŠĐģĐŗĐ°Ņ€Đ¸Ņ": { + "edges": { + "from": [ + "country:BG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Đ‘ŅŠĐģĐŗĐ°Ņ€Đ¸Ņ" + } + }, + "nym:ĐšĐ°ĐˇĐ°Ņ…ŅŅ‚Đ°ĐŊ": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐšĐ°ĐˇĐ°Ņ…ŅŅ‚Đ°ĐŊ" + } + }, + "nym:ĐšĐ¸Ņ€ĐŗĐ¸ĐˇĐ¸Ņ": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐšĐ¸Ņ€ĐŗĐ¸ĐˇĐ¸Ņ" + } + }, + "nym:ĐšĐžŅĐžĐ˛Đž": { + "edges": { + "from": [ + "country:XK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐšĐžŅĐžĐ˛Đž" + } + }, + "nym:ĐšŅ‹Ņ€ĐŗŅ‹ĐˇŅŅ‚Đ°ĐŊ": { + "edges": { + "from": [ + "country:KG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐšŅ‹Ņ€ĐŗŅ‹ĐˇŅŅ‚Đ°ĐŊ" + } + }, + "nym:МаĐēĐĩĐ´ĐžĐŊĐ¸Ņ˜Đ°": { + "edges": { + "from": [ + "country:MK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "МаĐēĐĩĐ´ĐžĐŊĐ¸Ņ˜Đ°" + } + }, + "nym:МоĐŊĐŗĐžĐģ ĐŖĐģҁ": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "МоĐŊĐŗĐžĐģ ĐŖĐģҁ" + } + }, + "nym:МоĐŊĐŗĐžĐģ ҃Đģҁ": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "МоĐŊĐŗĐžĐģ ҃Đģҁ" + } + }, + "nym:Đ ĐžŅŅĐ¸ĐšŅĐēĐ°Ņ": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Đ ĐžŅŅĐ¸ĐšŅĐēĐ°Ņ" + } + }, + "nym:Đ ĐžŅŅĐ¸Ņ": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Đ ĐžŅŅĐ¸Ņ" + } + }, + "nym:Đ ĐžŅŅĐ¸Ņ1": { + "edges": { + "from": [ + "country:RU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Đ ĐžŅŅĐ¸Ņ1" + } + }, + "nym:ĐĄĐžŅŽĐˇ ХОвĐĩ҂ҁĐēĐ¸Ņ… ĐĄĐžŅ†Đ¸Đ°ĐģĐ¸ŅŅ‚Đ¸Ņ‡ĐĩҁĐēĐ¸Ņ… Đ ĐĩҁĐŋŅƒĐąĐģиĐē": { + "edges": { + "from": [ + "country:SU" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐĄĐžŅŽĐˇ ХОвĐĩ҂ҁĐēĐ¸Ņ… ĐĄĐžŅ†Đ¸Đ°ĐģĐ¸ŅŅ‚Đ¸Ņ‡ĐĩҁĐēĐ¸Ņ… Đ ĐĩҁĐŋŅƒĐąĐģиĐē" + } + }, + "nym:ĐĄŅ€ĐąĐ¸Ņ˜Đ°": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐĄŅ€ĐąĐ¸Ņ˜Đ°" + } + }, + "nym:ĐĄŅ€ĐąĐ¸Ņ˜Đ° Srbija": { + "edges": { + "from": [ + "country:RS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐĄŅ€ĐąĐ¸Ņ˜Đ° Srbija" + } + }, + "nym:ĐĸĐžŌˇĐ¸ĐēĐ¸ŅŅ‚ĐžĐŊ": { + "edges": { + "from": [ + "country:TJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐĸĐžŌˇĐ¸ĐēĐ¸ŅŅ‚ĐžĐŊ" + } + }, + "nym:ĐŖĐēŅ€Đ°Ņ—ĐŊа": { + "edges": { + "from": [ + "country:UA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐŖĐēŅ€Đ°Ņ—ĐŊа" + } + }, + "nym:ĐĻŅ€ĐŊа Đ“ĐžŅ€Đ°": { + "edges": { + "from": [ + "country:ME" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ĐĻŅ€ĐŊа Đ“ĐžŅ€Đ°" + } + }, + "nym:ŌšĐ°ĐˇĐ°Ō›ŅŅ‚Đ°ĐŊ": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ŌšĐ°ĐˇĐ°Ō›ŅŅ‚Đ°ĐŊ" + } + }, + "nym:Õ€ÕĄÕĩÕĄÕŊÕŋÕĄÕļ": { + "edges": { + "from": [ + "country:AM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Õ€ÕĄÕĩÕĄÕŊÕŋÕĄÕļ" + } + }, + "nym:ישראל": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ישראל" + } + }, + "nym:ØĨØąØĒØąŲŠØ§": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĨØąØĒØąŲŠØ§" + } + }, + "nym:ØĨØŗØąØ§ØĻŲŠŲ„": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĨØŗØąØ§ØĻŲŠŲ„" + } + }, + "nym:ØĨØŗØąØ§ØĻŲŠŲ„ ישראל": { + "edges": { + "from": [ + "country:IL" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĨØŗØąØ§ØĻŲŠŲ„ ישראל" + } + }, + "nym:Ø§ŲØēØ§Ų†ØŗØĒØ§Ų†": { + "edges": { + "from": [ + "country:AF" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§ŲØēØ§Ų†ØŗØĒØ§Ų†" + } + }, + "nym:Ø§Ų„ØŖØąØ¯Ų†": { + "edges": { + "from": [ + "country:JO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØŖØąØ¯Ų†" + } + }, + "nym:Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ" + } + }, + "nym:Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ Ø§Ų„ØšØąØ¨ŲŠŲ‘ØŠ Ø§Ų„Ų…ØĒŲ‘Ø­Ø¯ØŠ": { + "edges": { + "from": [ + "country:AE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ Ø§Ų„ØšØąØ¨ŲŠŲ‘ØŠ Ø§Ų„Ų…ØĒŲ‘Ø­Ø¯ØŠ" + } + }, + "nym:Ø§Ų„Ø¨Ø­ØąŲŠŲ†": { + "edges": { + "from": [ + "country:BH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„Ø¨Ø­ØąŲŠŲ†" + } + }, + "nym:Ø§Ų„ØŦØ˛Ø§ØĻØą": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØŦØ˛Ø§ØĻØą" + } + }, + "nym:Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ" + } + }, + "nym:Ø§Ų„ØŗŲˆØ¯Ø§Ų†": { + "edges": { + "from": [ + "country:SD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØŗŲˆØ¯Ø§Ų†" + } + }, + "nym:Ø§Ų„ØĩŲˆŲ…Ø§Ų„": { + "edges": { + "from": [ + "country:SO" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØĩŲˆŲ…Ø§Ų„" + } + }, + "nym:Ø§Ų„ØšØąØ§Ų‚": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØšØąØ§Ų‚" + } + }, + "nym:Ø§Ų„ØšØąØ§Ų‚â€Ž": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ØšØąØ§Ų‚â€Ž" + } + }, + "nym:Ø§Ų„ŲƒŲˆŲŠØĒ": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ŲƒŲˆŲŠØĒ" + } + }, + "nym:Ø§Ų„Ų…ØēØąØ¨": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„Ų…ØēØąØ¨" + } + }, + "nym:Ø§Ų„Ų…Ų…Ų„ŲƒØŠ Ø§Ų„ØšØąØ¨ŲŠØŠ Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ": { + "edges": { + "from": [ + "country:SA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„Ų…Ų…Ų„ŲƒØŠ Ø§Ų„ØšØąØ¨ŲŠØŠ Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ" + } + }, + "nym:Ø§Ų„Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØŠ": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØŠ" + } + }, + "nym:Ø§Ų„ŲŠŲ…Ų†": { + "edges": { + "from": [ + "country:YE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§Ų„ŲŠŲ…Ų†" + } + }, + "nym:Ø§ÛŒØąØ§Ų†": { + "edges": { + "from": [ + "country:IR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø§ÛŒØąØ§Ų†" + } + }, + "nym:Ø¨ØąŲˆŲ†ŲŠ": { + "edges": { + "from": [ + "country:BN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø¨ØąŲˆŲ†ŲŠ" + } + }, + "nym:ØĒشاد": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĒشاد" + } + }, + "nym:ØĒشاد‎": { + "edges": { + "from": [ + "country:TD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĒشاد‎" + } + }, + "nym:ØĒŲˆŲ†Øŗ": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØĒŲˆŲ†Øŗ" + } + }, + "nym:ØŦØ˛ Ø§Ų„Ų‚Ų…Øą": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØŦØ˛ Ø§Ų„Ų‚Ų…Øą" + } + }, + "nym:ØŦØ˛Øą Ø§Ų„Ų‚Ų…Øą": { + "edges": { + "from": [ + "country:KM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØŦØ˛Øą Ø§Ų„Ų‚Ų…Øą" + } + }, + "nym:ØŦŲŠØ¨ŲˆØĒ؊": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØŦŲŠØ¨ŲˆØĒ؊" + } + }, + "nym:ØŦŲŠØ¨ŲˆØĒŲŠâ€Ž": { + "edges": { + "from": [ + "country:DJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØŦŲŠØ¨ŲˆØĒŲŠâ€Ž" + } + }, + "nym:Ø¯ŲˆŲ„ØŠ Ø§Ų„ŲƒŲˆŲŠØĒ": { + "edges": { + "from": [ + "country:KW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ø¯ŲˆŲ„ØŠ Ø§Ų„ŲƒŲˆŲŠØĒ" + } + }, + "nym:ØŗŲˆØąŲŠØŠ": { + "edges": { + "from": [ + "country:SY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØŗŲˆØąŲŠØŠ" + } + }, + "nym:ØšŲ…Ø§Ų†": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØšŲ…Ø§Ų†" + } + }, + "nym:ØšŲŲ…Ø§Ų†": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ØšŲŲ…Ø§Ų†" + } + }, + "nym:ŲŲ„ØŗØˇŲŠŲ†": { + "edges": { + "from": [ + "territory:PS" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ŲŲ„ØŗØˇŲŠŲ†" + } + }, + "nym:Ų‚Ø§Ø˛Ø§Ų‚ØŗØĒØ§Ų†": { + "edges": { + "from": [ + "country:KZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų‚Ø§Ø˛Ø§Ų‚ØŗØĒØ§Ų†" + } + }, + "nym:Ų‚ØˇØą": { + "edges": { + "from": [ + "country:QA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų‚ØˇØą" + } + }, + "nym:Ų„Ø¨Ų†Ø§Ų†": { + "edges": { + "from": [ + "country:LB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų„Ø¨Ų†Ø§Ų†" + } + }, + "nym:Ų„ØĩØ­ØąØ§ØĄ Ø§Ų„ØēØąØ¨ŲŠØŠ": { + "edges": { + "from": [ + "territory:EH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų„ØĩØ­ØąØ§ØĄ Ø§Ų„ØēØąØ¨ŲŠØŠ" + } + }, + "nym:Ų„ŲŠØ¨ŲŠØ§": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų„ŲŠØ¨ŲŠØ§" + } + }, + "nym:Ų…ØĩØą": { + "edges": { + "from": [ + "country:EG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų…ØĩØą" + } + }, + "nym:Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØ§": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØ§" + } + }, + "nym:ŲžØ§ÚŠØŗØĒØ§Ų†": { + "edges": { + "from": [ + "country:PK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ŲžØ§ÚŠØŗØĒØ§Ų†" + } + }, + "nym:⤍āĨ‡ā¤Ēā¤žā¤˛": { + "edges": { + "from": [ + "country:NP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "⤍āĨ‡ā¤Ēā¤žā¤˛" + } + }, + "nym:ā¤Ģā¤ŧā¤ŋ⤜āĨ€": { + "edges": { + "from": [ + "country:FJ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¤Ģā¤ŧā¤ŋ⤜āĨ€" + } + }, + "nym:ā¤­ā¤žā¤°ā¤¤": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¤­ā¤žā¤°ā¤¤" + } + }, + "nym:ā¤­ā¤žā¤°ā¤¤ ā¤—ā¤Ŗā¤°ā¤žā¤œāĨā¤¯": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¤­ā¤žā¤°ā¤¤ ā¤—ā¤Ŗā¤°ā¤žā¤œāĨā¤¯" + } + }, + "nym:ā¤­ā¤žā¤°ā¤¤ā¤ŽāĨ": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¤­ā¤žā¤°ā¤¤ā¤ŽāĨ" + } + }, + "nym:⤭āĨ‚ā¤Ÿā¤žā¤¨": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "⤭āĨ‚ā¤Ÿā¤žā¤¨" + } + }, + "nym:ā¤ļ⤰āĨā¤¨ā¤ŽāĨ": { + "edges": { + "from": [ + "country:SR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¤ļ⤰āĨā¤¨ā¤ŽāĨ" + } + }, + "nym:āĻŦāĻžāĻ‚āϞāĻžāĻĻ⧇āĻļ": { + "edges": { + "from": [ + "country:BD" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āĻŦāĻžāĻ‚āϞāĻžāĻĻ⧇āĻļ" + } + }, + "nym:āĻ­āĻžāϰāϤ": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āĻ­āĻžāϰāϤ" + } + }, + "nym:āĻ­āĻžā§°āϤ": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āĻ­āĻžā§°āϤ" + } + }, + "nym:ā¨­ā¨žā¨°ā¨¤": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¨­ā¨žā¨°ā¨¤" + } + }, + "nym:āĒ­āĒžāǰāǤ": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āĒ­āĒžāǰāǤ" + } + }, + "nym:āŦ­āŦžāŦ°āŦ¤": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŦ­āŦžāŦ°āŦ¤" + } + }, + "nym:āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ" + } + }, + "nym:āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯" + } + }, + "nym:āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯ āŽ•ā¯āŽŸāŽŋāŽ¯āŽ°āŽšā¯": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯ āŽ•ā¯āŽŸāŽŋāŽ¯āŽ°āŽšā¯" + } + }, + "nym:āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯āŽ•ā¯": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯āŽ•ā¯" + } + }, + "nym:āŽŸāŽŋāŽ¯āŽ°āŽšā¯": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽŸāŽŋāŽ¯āŽ°āŽšā¯" + } + }, + "nym:āŽĒāŽžāŽ°āŽ¤āŽŽā¯": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽĒāŽžāŽ°āŽ¤āŽŽā¯" + } + }, + "nym:āŽŽāŽ˛ā¯‡āŽšāŽŋāŽ¯āŽž": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŽŽāŽ˛ā¯‡āŽšāŽŋāŽ¯āŽž" + } + }, + "nym:ā°­ā°žā°°ā°¤ ā°Ļāą‡ā°ļā°‚": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā°­ā°žā°°ā°¤ ā°Ļāą‡ā°ļā°‚" + } + }, + "nym:ā˛­ā˛žā˛°ā˛¤": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā˛­ā˛žā˛°ā˛¤" + } + }, + "nym:ā´­ā´žā´°ā´¤ā´‚": { + "edges": { + "from": [ + "country:IN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā´­ā´žā´°ā´¤ā´‚" + } + }, + "nym:āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇ": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇ" + } + }, + "nym:āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇ āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇ āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ" + } + }, + "nym:āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇāˇ€": { + "edges": { + "from": [ + "country:LK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āˇāˇŠâ€āļģ⎓ āļŊāļ‚āļšāˇāˇ€" + } + }, + "nym:ā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨āš„ā¸—ā¸ĸ": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨āš„ā¸—ā¸ĸ" + } + }, + "nym:ā¸Ŗā¸˛ā¸Šā¸­ā¸˛ā¸“ā¸˛ā¸ˆā¸ąā¸ā¸Ŗāš„ā¸—ā¸ĸ": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ā¸Ŗā¸˛ā¸Šā¸­ā¸˛ā¸“ā¸˛ā¸ˆā¸ąā¸ā¸Ŗāš„ā¸—ā¸ĸ" + } + }, + "nym:āš€ā¸Ąā¸ˇā¸­ā¸‡āš„ā¸—ā¸ĸ": { + "edges": { + "from": [ + "country:TH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āš€ā¸Ąā¸ˇā¸­ā¸‡āš„ā¸—ā¸ĸ" + } + }, + "nym:āē›āē°āģ€āē—āē”āēĨāē˛āē§": { + "edges": { + "from": [ + "country:LA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āē›āē°āģ€āē—āē”āēĨāē˛āē§" + } + }, + "nym:āŊ āŊ–āž˛āŊ´āŊ‚āŧ‹āŊĄāŊ´āŊŖ": { + "edges": { + "from": [ + "country:BT" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "āŊ āŊ–āž˛āŊ´āŊ‚āŧ‹āŊĄāŊ´āŊŖ" + } + }, + "nym:မá€ŧနá€ēမá€Ŧ": { + "edges": { + "from": [ + "country:MM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "မá€ŧနá€ēမá€Ŧ" + } + }, + "nym:ქაáƒĨართველო": { + "edges": { + "from": [ + "country:GE" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ქაáƒĨართველო" + } + }, + "nym:áŠĸá‰ĩዮáŒĩá‹Ģ": { + "edges": { + "from": [ + "country:ET" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "áŠĸá‰ĩዮáŒĩá‹Ģ" + } + }, + "nym:ኤርá‰ĩáˆĢ": { + "edges": { + "from": [ + "country:ER" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ኤርá‰ĩáˆĢ" + } + }, + "nym:កម្ពážģជážļ": { + "edges": { + "from": [ + "country:KH" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "កម្ពážģជážļ" + } + }, + "nym:᠎ᠤ᠊ᠭᠤᠯ ᠤᠯᠤᠰ": { + "edges": { + "from": [ + "country:MN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "᠎ᠤ᠊ᠭᠤᠯ ᠤᠯᠤᠰ" + } + }, + "nym:‎": { + "edges": { + "from": [ + "country:IQ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "‎" + } + }, + "nym:‘Umān": { + "edges": { + "from": [ + "country:OM" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "‘Umān" + } + }, + "nym:â´°â´ŗâ´°âĩĄâĩ›": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "â´°â´ŗâ´°âĩĄâĩ›" + } + }, + "nym:â´°âĩŽâĩ”âĩ”âĩ“â´Ŋ": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "â´°âĩŽâĩ”âĩ”âĩ“â´Ŋ" + } + }, + "nym:â´ˇâĩŖâ´°âĩĸâ´ģâĩ”": { + "edges": { + "from": [ + "country:DZ" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "â´ˇâĩŖâ´°âĩĸâ´ģâĩ”" + } + }, + "nym:âĩâĩ‰â´ąâĩĸâ´°": { + "edges": { + "from": [ + "country:LY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "âĩâĩ‰â´ąâĩĸâ´°" + } + }, + "nym:âĩâĩŽâĩ–âĩ”âĩ‰â´ą": { + "edges": { + "from": [ + "country:MA" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "âĩâĩŽâĩ–âĩ”âĩ‰â´ą" + } + }, + "nym:âĩŽâĩ“âĩ”âĩ‰âĩœâ´°âĩ": { + "edges": { + "from": [ + "country:MR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "âĩŽâĩ“âĩ”âĩ‰âĩœâ´°âĩ" + } + }, + "nym:âĩœâĩ“âĩâĩ™": { + "edges": { + "from": [ + "country:TN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "âĩœâĩ“âĩâĩ™" + } + }, + "nym:中华": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "中华" + } + }, + "nym:中å›Ŋ": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "中å›Ŋ" + } + }, + "nym:中å›Ŋ/中华": { + "edges": { + "from": [ + "country:CN" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "中å›Ŋ/中华" + } + }, + "nym:ä¸­č¯æ°‘åœ‹": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ä¸­č¯æ°‘åœ‹" + } + }, + "nym:å°įŖ": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "å°įŖ" + } + }, + "nym:æ–°åŠ åĄ": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "æ–°åŠ åĄ" + } + }, + "nym:æ–°åŠ åĄå…ąå’Œå›Ŋ": { + "edges": { + "from": [ + "country:SG" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "æ–°åŠ åĄå…ąå’Œå›Ŋ" + } + }, + "nym:æ—ĨæœŦ": { + "edges": { + "from": [ + "country:JP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "æ—ĨæœŦ" + } + }, + "nym:č‡ēၪ": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "č‡ēၪ" + } + }, + "nym:č‡ēၪ/å°įŖ": { + "edges": { + "from": [ + "territory:TW" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "č‡ēၪ/å°įŖ" + } + }, + "nym:éĻ™æ¸¯": { + "edges": { + "from": [ + "territory:HK" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "éĻ™æ¸¯" + } + }, + "nym:éŠŦæĨčĨŋäēš": { + "edges": { + "from": [ + "country:MY" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "éŠŦæĨčĨŋäēš" + } + }, + "nym:남한": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "남한" + } + }, + "nym:ëļėĄ°ė„ ": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ëļėĄ°ė„ " + } + }, + "nym:ėĄ°ė„  / 朝鎎": { + "edges": { + "from": [ + "country:KP" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "ėĄ°ė„  / 朝鎎" + } + }, + "nym:한ęĩ­ / 韓國": { + "edges": { + "from": [ + "country:KR" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": false, + "stable-name": false + }, + "names": { + "cy": false, + "en-GB": "한ęĩ­ / 韓國" + } + }, + "territory:AE-AJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ajman" + } + }, + "territory:AE-AZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Abu Dhabi" + } + }, + "territory:AE-DU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Dubai" + } + }, + "territory:AE-FU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Fujairah" + } + }, + "territory:AE-RK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ras al-Khaimah" + } + }, + "territory:AE-SH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sharjah" + } + }, + "territory:AE-UQ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Umm al-Quwain" + } + }, + "territory:AI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Anguilla" + } + }, + "territory:AQ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Antarctica" + } + }, + "territory:AS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "American Samoa" + } + }, + "territory:AW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Aruba" + } + }, + "territory:AX": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Åland Islands" + } + }, + "territory:BAT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "British Antarctic Territory" + } + }, + "territory:BL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint BarthÊlemy" + } + }, + "territory:BM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bermuda" + } + }, + "territory:BQ-BO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bonaire" + } + }, + "territory:BQ-SA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saba" + } + }, + "territory:BQ-SE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sint Eustatius" + } + }, + "territory:BV": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Bouvet Island" + } + }, + "territory:CC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cocos (Keeling) Islands" + } + }, + "territory:CK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cook Islands" + } + }, + "territory:CW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Curaçao" + } + }, + "territory:CX": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Christmas Island" + } + }, + "territory:EH": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Western Sahara" + } + }, + "territory:ES-CE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ceuta" + } + }, + "territory:ES-ML": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Melilla" + } + }, + "territory:FK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Falkland Islands" + } + }, + "territory:FO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Faroe Islands" + } + }, + "territory:GF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Guiana" + } + }, + "territory:GG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guernsey" + } + }, + "territory:GI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Gibraltar" + } + }, + "territory:GL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Greenland" + } + }, + "territory:GP": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guadeloupe" + } + }, + "territory:GS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "South Georgia and South Sandwich Islands" + } + }, + "territory:GU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Guam" + } + }, + "territory:HK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Hong Kong" + } + }, + "territory:HM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Heard Island and McDonald Islands" + } + }, + "territory:IM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Isle of Man" + } + }, + "territory:IO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "British Indian Ocean Territory" + } + }, + "territory:JE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jersey" + } + }, + "territory:KY": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Cayman Islands" + } + }, + "territory:MF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint-Martin (French part)" + } + }, + "territory:MO": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Macao" + } + }, + "territory:MP": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Northern Mariana Islands" + } + }, + "territory:MQ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Martinique" + } + }, + "territory:MS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Montserrat" + } + }, + "territory:NC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "New Caledonia" + } + }, + "territory:NF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Norfolk Island" + } + }, + "territory:NU": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Niue" + } + }, + "territory:PF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Polynesia" + } + }, + "territory:PM": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Pierre and Miquelon" + } + }, + "territory:PN": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Pitcairn, Henderson, Ducie and Oeno Islands" + } + }, + "territory:PR": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Puerto Rico" + } + }, + "territory:PS": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Occupied Palestinian Territories" + } + }, + "territory:RE": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "RÊunion" + } + }, + "territory:SH-AC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Ascension" + } + }, + "territory:SH-HL": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Saint Helena" + } + }, + "territory:SH-TA": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tristan da Cunha" + } + }, + "territory:SJ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Svalbard and Jan Mayen" + } + }, + "territory:SX": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Sint Maarten (Dutch part)" + } + }, + "territory:TC": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Turks and Caicos Islands" + } + }, + "territory:TF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "French Southern Territories" + } + }, + "territory:TK": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Tokelau" + } + }, + "territory:TW": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Taiwan" + } + }, + "territory:UM-67": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Johnston Atoll" + } + }, + "territory:UM-71": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Midway Islands" + } + }, + "territory:UM-76": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Navassa Island" + } + }, + "territory:UM-79": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Wake Island" + } + }, + "territory:UM-81": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Baker Island" + } + }, + "territory:UM-84": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Howland Island" + } + }, + "territory:UM-86": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Jarvis Island" + } + }, + "territory:UM-89": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Kingman Reef" + } + }, + "territory:UM-95": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Palmyra Atoll" + } + }, + "territory:VG": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "British Virgin Islands" + } + }, + "territory:VI": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "United States Virgin Islands" + } + }, + "territory:WF": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Wallis and Futuna" + } + }, + "territory:XQZ": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Akrotiri" + } + }, + "territory:XXD": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Dhekelia" + } + }, + "territory:YT": { + "edges": { + "from": [ + ] + }, + "meta": { + "canonical": true, + "canonical-mask": 1, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Mayotte" + } + }, + "uk:ENG": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "England" + } + }, + "uk:GBN": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Great Britain" + } + }, + "uk:NIR": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Northern Ireland" + } + }, + "uk:SCT": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Scotland" + } + }, + "uk:WLS": { + "edges": { + "from": [ + "country:GB" + ] + }, + "meta": { + "canonical": false, + "canonical-mask": 0, + "display-name": true, + "stable-name": true + }, + "names": { + "cy": false, + "en-GB": "Wales" + } + } +} diff --git a/notifications_utils/countries/_data/synonyms.json b/notifications_utils/countries/_data/synonyms.json new file mode 100644 index 000000000..69c712f2a --- /dev/null +++ b/notifications_utils/countries/_data/synonyms.json @@ -0,0 +1,58 @@ +{ + "England": "United Kingdom", + "Northern Ireland": "United Kingdom", + "Scotland": "United Kingdom", + "Wales": "United Kingdom", + "ROI": "Ireland", + "Irish Republic": "Ireland", + "Rep of Ireland": "Ireland", + "South Ireland": "Ireland", + "Southern Ireland": "Ireland", + "N Ireland": "United Kingdom", + "North Ireland": "United Kingdom", + "GBR": "United Kingdom", + "United States America": "United States", + "America": "United States", + "Macedonia": "North Macedonia", + "Autonomous Region of the Azores": "Azores", + "Islas Canarias": "Canary Islands", + "Canaries": "Canary Islands", + "Autonomous Region of Madeira": "Madeira", + "RegiÃŖo AutÃŗnoma da Madeira": "Madeira", + "Islas Baleares": "Balearic Islands", + "Illes Balears": "Balearic Islands", + "Corse": "Corsica", + "Burma": "Myanmar (Burma)", + "Czechoslovakia": "Czechia", + "East Germany": "Germany", + "Easter Island": "Easter Island", + "Falkland": "Falkland Islands", + "The Falklands": "Falkland Islands", + "The Falkland Islands": "Falkland Islands", + "Hawaii": "United States", + "Khazakhstan": "Kazakhstan", + "Korea": "South Korea", + "Macau": "Macao", + "Myanmar": "Myanmar (Burma)", + "New Zeeland": "New Zealand", + "NI": "United Kingdom", + "Pitcairn Island": "Pitcairn, Henderson, Ducie and Oeno Islands", + "Henderson Island": "Pitcairn, Henderson, Ducie and Oeno Islands", + "Ducie Island": "Pitcairn, Henderson, Ducie and Oeno Islands", + "Oeno Island": "Pitcairn, Henderson, Ducie and Oeno Islands", + "Republic of China": "Taiwan", + "Republik Österreich": "Austria", + "RÊpublique Islamique de Mauritanie": "Mauritania", + "Saint Helena": "Saint Helena", + "St Helena": "Saint Helena", + "Swaziland": "Eswatini", + "the south sandwich islands": "South Georgia and the South Sandwich Islands", + "the sandwich islands": "South Georgia and the South Sandwich Islands", + "South Georgia": "South Georgia and the South Sandwich Islands", + "Tristan": "Tristan da Cunha", + "Vatican": "Vatican City", + "West Germany": "Germany", + "Saint Kitts and Nevis": "St Kitts and Nevis", + "Saint Kitts": "St Kitts and Nevis", + "St Kitts": "St Kitts and Nevis" +} diff --git a/notifications_utils/countries/_data/uk-islands.txt b/notifications_utils/countries/_data/uk-islands.txt new file mode 100644 index 000000000..d678109e1 --- /dev/null +++ b/notifications_utils/countries/_data/uk-islands.txt @@ -0,0 +1,8 @@ +Alderney +Brecqhou +Guernsey +Herm +Isle of Man +Jersey +Jethou +Sark diff --git a/notifications_utils/countries/_data/welsh-names.json b/notifications_utils/countries/_data/welsh-names.json new file mode 100644 index 000000000..edfbf2ac3 --- /dev/null +++ b/notifications_utils/countries/_data/welsh-names.json @@ -0,0 +1,103 @@ +{ + "Affganistan": "Afghanistan", + "Antigwa a Barbiwda": "Antigua and Barbuda", + "Yr Ariannin": "Argentina", + "Awstralia": "Australia", + "Awstria": "Austria", + "Aserbaijan": "Azerbaijan", + "Y Bahamas": "The Bahamas", + "Belarws": "Belarus", + "Gwlad Belg": "Belgium", + "Bhwtan": "Bhutan", + "Bolifia": "Bolivia", + "Bosnia a Hercegovina": "Bosnia and Herzegovina", + "Brasil": "Brazil", + "Bwlgaria": "Bulgaria", + "Bwrwndi": "Burundi", + "CamerÅĩn": "Cameroon", + "Cabo Verde": "Cape Verde", + "Gweriniaeth Canolbarth Affrica": "Central African Republic", + "Tchad": "Chad", + "Tsieina": "China", + "Y Comoros": "Comoros", + "Ciwba": "Cuba", + "Y Weriniaeth Tsiec": "Czechia", + "Gweriniaeth Ddemocrataidd Congo": "Congo (Democratic Republic)", + "Denmarc": "Denmark", + "Gweriniaeth Dominica": "Dominican Republic", + "Dwyrain Timor": "East Timor", + "Ecwador": "Ecuador", + "Yr Aifft": "Egypt", + "Gini Gyhydeddol": "Equatorial Guinea", + "Ffiji": "Fiji", + "Y Ffindir": "Finland", + "Ffrainc": "France", + "Y Gambia": "The Gambia", + "Yr Alban": "United Kingdom", + "Yr Almaen": "Germany", + "Gwlad Groeg": "Greece", + "Gini": "Guinea", + "GuinÊ-Bissau": "Guinea-Bissau", + "Gaiana": "Guyana", + "Hondwras": "Honduras", + "Hwngari": "Hungary", + "Gwlad yr IÃĸ": "Iceland", + "Irac": "Iraq", + "Iwerddon": "Ireland", + "Yr Eidal": "Italy", + "Iorddonen": "Jordan", + "Kazakstan": "Kazakhstan", + "Latfia": "Latvia", + "Libanus": "Lebanon", + "Libia": "Libya", + "Lithwania": "Lithuania", + "Lwcsembwrg": "Luxembourg", + "Madagasgar": "Madagascar", + "Ynysoedd Marshall": "Marshall Islands", + "Mecsico": "Mexico", + "Moldofa": "Moldova", + "Moroco": "Morocco", + "Mosambic": "Mozambique", + "Yr Iseldiroedd": "Netherlands", + "Seland Newydd": "New Zealand", + "Nicaragwa": "Nicaragua", + "Gogledd Corea": "North Korea", + "Norwy": "Norway", + "Papua Guinea Newydd": "Papua New Guinea", + "ParagwÃĸi": "Paraguay", + "Periw": "Peru", + "Pilipinas": "Philippines", + "Gwlad Pwyl": "Poland", + "Portiwgal": "Portugal", + "Gweriniaeth y Congo": "Congo", + "Gweriniaeth Macedonia": "North Macedonia", + "Gogledd Macedonia": "North Macedonia", + "Rwmania": "Romania", + "Rwsia": "Russia", + "Saint Kitts a Nevis": "St Kitts and Nevis", + "St Kitts a Nevis": "St Kitts and Nevis", + "Saint Vincent a’r Grenadines": "St Vincent", + "SÃŖo TomÊ a Príncipe": "Sao Tome and Principe", + "SÊnÊgal": "Senegal", + "Slofacia": "Slovakia", + "Slofenia": "Slovenia", + "Ynysoedd Solomon": "Solomon Islands", + "De Affrica": "South Africa", + "De Corea": "South Korea", + "De Sudan": "South Sudan", + "Sbaen": "Spain", + "Swrinam": "Suriname", + "Gwlad Swazi": "Eswatini", + "Y Swistir": "Switzerland", + "Gwlad Thai": "Thailand", + "Trinidad a Thobago": "Trinidad and Tobago", + "Twrci": "Turkey", + "Twfalw": "Tuvalu", + "WcrÃĄin": "Ukraine", + "Yr Emiradau Arabaidd Unedig": "United Arab Emirates", + "Y Deyrnas Unedig": "United Kingdom", + "Unol Daleithiau America": "United States", + "WrwgwÃĄi": "Uruguay", + "Feneswela": "Venezuela", + "Fietnam": "Vietnam" +} diff --git a/notifications_utils/countries/data.py b/notifications_utils/countries/data.py new file mode 100644 index 000000000..0acabb22d --- /dev/null +++ b/notifications_utils/countries/data.py @@ -0,0 +1,67 @@ +import json +import os + + +def _load_data(filename): + with open(os.path.join(os.path.dirname(__file__), "_data", filename)) as contents: + if filename.endswith(".json"): + return json.load(contents) + return [line.strip() for line in contents.readlines()] + + +def find_canonical(item, graph, key): + if item["meta"]["canonical"]: + return key, item["names"]["en-GB"] + return find_canonical( + graph[item["edges"]["from"][0]], + graph, + key, + ) + + +# Copied from +# https://github.com/alphagov/govuk-country-and-territory-autocomplete +# /blob/b61091a502983fd2a77b3cdb5f94a604412eb093 +# /dist/location-autocomplete-graph.json +_graph = _load_data("location-autocomplete-graph.json") + +UK = "United Kingdom" + +ENDED_COUNTRIES = _load_data("ended-countries.json") +ADDITIONAL_SYNONYMS = list(_load_data("synonyms.json").items()) +WELSH_NAMES = list(_load_data("welsh-names.json").items()) +_UK_ISLANDS_LIST = _load_data("uk-islands.txt") +_EUROPEAN_ISLANDS_LIST = _load_data("european-islands.txt") + +CURRENT_AND_ENDED_COUNTRIES_AND_TERRITORIES = [ + find_canonical(item, _graph, item["names"]["en-GB"]) for item in _graph.values() +] + +COUNTRIES_AND_TERRITORIES = [] + +for synonym, canonical in CURRENT_AND_ENDED_COUNTRIES_AND_TERRITORIES: + if canonical in _UK_ISLANDS_LIST: + COUNTRIES_AND_TERRITORIES.append((synonym, UK)) + elif canonical in ENDED_COUNTRIES: + succeeding_country = ENDED_COUNTRIES[canonical] + if succeeding_country: + COUNTRIES_AND_TERRITORIES.append((synonym, succeeding_country)) + COUNTRIES_AND_TERRITORIES.append((canonical, succeeding_country)) + else: + COUNTRIES_AND_TERRITORIES.append((synonym, canonical)) + +UK_ISLANDS = [(synonym, UK) for synonym in _UK_ISLANDS_LIST] + +EUROPEAN_ISLANDS = [(synonym, synonym) for synonym in _EUROPEAN_ISLANDS_LIST] + +# Copied from https://www.royalmail.com/international-zones#europe +# Modified to use the canonical names for countries where incorrect +ROYAL_MAIL_EUROPEAN = _load_data("europe.txt") + + +class Postage: + UK = "united-kingdom" + FIRST = "first" + SECOND = "second" + EUROPE = "europe" + REST_OF_WORLD = "rest-of-world" diff --git a/notifications_utils/field.py b/notifications_utils/field.py new file mode 100644 index 000000000..c0ea50216 --- /dev/null +++ b/notifications_utils/field.py @@ -0,0 +1,208 @@ +import re + +from markupsafe import Markup +from ordered_set import OrderedSet + +from notifications_utils.formatters import ( + escape_html, + strip_and_remove_obscure_whitespace, + strip_html, + unescaped_formatted_list, +) +from notifications_utils.insensitive_dict import InsensitiveDict + + +class Placeholder: + def __init__(self, body): + # body shouldn’t include leading/trailing brackets, like (( and )) + self.body = body.lstrip("(").rstrip(")") + + @classmethod + def from_match(cls, match): + return cls(match.group(0)) + + def is_conditional(self): + return "??" in self.body + + @property + def name(self): + # for non conditionals, name equals body + return self.body.split("??")[0] + + @property + def conditional_text(self): + if self.is_conditional(): + # ((a?? b??c)) returns " b??c" + return "??".join(self.body.split("??")[1:]) + else: + raise ValueError("{} not conditional".format(self)) + + def get_conditional_body(self, show_conditional): + # note: unsanitised/converted + if self.is_conditional(): + return self.conditional_text if str2bool(show_conditional) else "" + else: + raise ValueError("{} not conditional".format(self)) + + def __repr__(self): + return "Placeholder({})".format(self.body) + + +class Field: + """ + An instance of Field represents a string of text which may contain + placeholders. + + If values are provided the field replaces the placeholders with the + corresponding values. If a value for a placeholder is missing then + the field will highlight the placeholder by wrapping it in some HTML. + + A template can have several fields, for example an email template + has a field for the body and a field for the subject. + """ + + placeholder_pattern = re.compile( + r"\({2}" # opening (( + r"([^()]+)" # body of placeholder - potentially standard or conditional. + r"\){2}" # closing )) + ) + placeholder_tag = "(({}))" + conditional_placeholder_tag = ( + "(({}??{}))" + ) + placeholder_tag_no_brackets = "{}" + placeholder_tag_redacted = "hidden" + + def __init__( + self, + content, + values=None, + with_brackets=True, + html="strip", + markdown_lists=False, + redact_missing_personalisation=False, + ): + self.content = content + self.values = values + self.markdown_lists = markdown_lists + if not with_brackets: + self.placeholder_tag = self.placeholder_tag_no_brackets + self.sanitizer = { + "strip": strip_html, + "escape": escape_html, + "passthrough": str, + }[html] + self.redact_missing_personalisation = redact_missing_personalisation + + def __str__(self): + if self.values: + return self.replaced + return self.formatted + + def __repr__(self): + return '{}("{}", {})'.format( + self.__class__.__name__, self.content, self.values + ) # TODO: more real + + def splitlines(self): + return str(self).splitlines() + + @property + def values(self): + return self._values + + @values.setter + def values(self, value): + self._values = InsensitiveDict(value) if value else {} + + def format_match(self, match): + placeholder = Placeholder.from_match(match) + + if self.redact_missing_personalisation: + return self.placeholder_tag_redacted + + if placeholder.is_conditional(): + return self.conditional_placeholder_tag.format( + placeholder.name, placeholder.conditional_text + ) + + return self.placeholder_tag.format(placeholder.name) + + def replace_match(self, match): + placeholder = Placeholder.from_match(match) + replacement = self.values.get(placeholder.name) + + if placeholder.is_conditional() and replacement is not None: + return placeholder.get_conditional_body(replacement) + + replaced_value = self.get_replacement(placeholder) + if replaced_value is not None: + return self.get_replacement(placeholder) + + return self.format_match(match) + + def get_replacement(self, placeholder): + replacement = self.values.get(placeholder.name) + if replacement is None: + return None + + if isinstance(replacement, list): + vals = ( + strip_and_remove_obscure_whitespace(str(val)) + for val in replacement + if val is not None + ) + vals = list(filter(None, vals)) + if not vals: + return "" + return self.sanitizer(self.get_replacement_as_list(vals)) + + return self.sanitizer(str(replacement)) + + def get_replacement_as_list(self, replacement): + if self.markdown_lists: + return "\n\n" + "\n".join("* {}".format(item) for item in replacement) + return unescaped_formatted_list(replacement, before_each="", after_each="") + + @property + def _raw_formatted(self): + return re.sub( + self.placeholder_pattern, self.format_match, self.sanitizer(self.content) + ) + + @property + def formatted(self): + return Markup(self._raw_formatted) + + @property + def placeholders(self): + if not getattr(self, "content", ""): + return set() + return OrderedSet( + Placeholder(body).name + for body in re.findall(self.placeholder_pattern, self.content) + ) + + @property + def replaced(self): + return re.sub( + self.placeholder_pattern, self.replace_match, self.sanitizer(self.content) + ) + + +class PlainTextField(Field): + """ + Use this where no HTML should be rendered in the outputted content, + even when no values have been passed in + """ + + placeholder_tag = "(({}))" + conditional_placeholder_tag = "(({}??{}))" + placeholder_tag_no_brackets = "{}" + placeholder_tag_redacted = "[hidden]" + + +def str2bool(value): + if not value: + return False + return str(value).lower() in ("yes", "y", "true", "t", "1", "include", "show") diff --git a/notifications_utils/formatters.py b/notifications_utils/formatters.py new file mode 100644 index 000000000..a2cb7c3d7 --- /dev/null +++ b/notifications_utils/formatters.py @@ -0,0 +1,349 @@ +import re +import string +import urllib +from html import _replace_charref, escape + +import bleach +import smartypants +from markupsafe import Markup + +from notifications_utils.sanitise_text import SanitiseSMS + +from . import email_with_smart_quotes_regex + +OBSCURE_ZERO_WIDTH_WHITESPACE = ( + "\u180E" # Mongolian vowel separator + "\u200B" # zero width space + "\u200C" # zero width non-joiner + "\u200D" # zero width joiner + "\u2060" # word joiner + "\uFEFF" # zero width non-breaking space +) + +OBSCURE_FULL_WIDTH_WHITESPACE = "\u00A0" # non breaking space + +ALL_WHITESPACE = ( + string.whitespace + OBSCURE_ZERO_WIDTH_WHITESPACE + OBSCURE_FULL_WIDTH_WHITESPACE +) + +govuk_not_a_link = re.compile(r"(^|\s)(#|\*|\^)?(GOV)\.(UK)(?!\/|\?|#)", re.IGNORECASE) + +smartypants.tags_to_skip = smartypants.tags_to_skip + ["a"] + +whitespace_before_punctuation = re.compile(r"[ \t]+([,\.])") + +hyphens_surrounded_by_spaces = re.compile( + r"\s+[-–—]{1,3}\s+" +) # check three different unicode hyphens + +multiple_newlines = re.compile(r"((\n)\2{2,})") + +HTML_ENTITY_MAPPING = ( + (" ", "👾đŸĻđŸĨ´"), + ("&", "➕đŸĻđŸĨ´"), + ("(", "â—€ī¸đŸĻđŸĨ´"), + (")", "â–ļī¸đŸĻđŸĨ´"), +) + +url = re.compile( + r"(?i)" # case insensitive + r"\b(?", value.strip()) + + +def add_prefix(body, prefix=None): + if prefix: + return "{}: {}".format(prefix.strip(), body) + return body + + +def make_link_from_url(linked_part, *, classes=""): + """ + Takes something which looks like a URL, works out which trailing characters shouldn’t + be considered part of the link and returns an HTML tag + + input: `http://example.com/foo_(bar)).` + output: `http://example.com/foo_(bar)).` + """ + CORRESPONDING_OPENING_CHARACTER_MAP = { + ")": "(", + "]": "[", + ".": None, + ",": None, + ":": None, + } + + trailing_characters = "" + + while ( + last_character := linked_part[-1] + ) in CORRESPONDING_OPENING_CHARACTER_MAP.keys(): + corresponding_opening_character = CORRESPONDING_OPENING_CHARACTER_MAP[ + last_character + ] + + if corresponding_opening_character: + count_opening_characters = linked_part.count( + corresponding_opening_character + ) + count_closing_characters = linked_part.count(last_character) + if count_opening_characters >= count_closing_characters: + break + + trailing_characters = linked_part[-1] + trailing_characters + linked_part = linked_part[:-1] + + return f"{create_sanitised_html_for_url(linked_part, classes=classes)}{trailing_characters}" + + +def autolink_urls(value, *, classes=""): + return Markup( + url.sub( + lambda match: make_link_from_url( + match.group(0), + classes=classes, + ), + value, + ) + ) + + +def create_sanitised_html_for_url(link, *, classes="", style=""): + """ + takes a link and returns an a tag to that link. does the quote/unquote dance to ensure that " quotes are escaped + correctly to prevent xss + + input: `http://foo.com/"bar"?x=1#2` + output: `http://foo.com/"bar"?x=1#2` + """ + link_text = link + + if not link.lower().startswith("http"): + link = f"http://{link}" + + class_attribute = f'class="{classes}" ' if classes else "" + style_attribute = f'style="{style}" ' if style else "" + + return ('{}').format( + class_attribute, + style_attribute, + urllib.parse.quote(urllib.parse.unquote(link), safe=":/?#=&;"), + link_text, + ) + + +def prepend_subject(body, subject): + return "# {}\n\n{}".format(subject, body) + + +def sms_encode(content): + return SanitiseSMS.encode(content) + + +def strip_html(value): + return bleach.clean(value, tags=[], strip=True) + + +""" +Re-implements html._charref but makes trailing semicolons non-optional +""" +_charref = re.compile(r"&(#[0-9]+;" r"|#[xX][0-9a-fA-F]+;" r"|[^\t\n\f <&#;]{1,32};)") + + +def unescape_strict(s): + """ + Re-implements html.unescape to use our own definition of `_charref` + """ + if "&" not in s: + return s + return _charref.sub(_replace_charref, s) + + +def escape_html(value): + if not value: + return value + value = str(value) + + for entity, temporary_replacement in HTML_ENTITY_MAPPING: + value = value.replace(entity, temporary_replacement) + + value = escape(unescape_strict(value), quote=False) + + for entity, temporary_replacement in HTML_ENTITY_MAPPING: + value = value.replace(temporary_replacement, entity) + + return value + + +def url_encode_full_stops(value): + return value.replace(".", "%2E") + + +def unescaped_formatted_list( + items, + conjunction="and", + before_each="‘", + after_each="’", + separator=", ", + prefix="", + prefix_plural="", +): + if prefix: + prefix += " " + if prefix_plural: + prefix_plural += " " + + if len(items) == 1: + return "{prefix}{before_each}{items[0]}{after_each}".format(**locals()) + elif items: + formatted_items = [ + "{}{}{}".format(before_each, item, after_each) for item in items + ] + + first_items = separator.join(formatted_items[:-1]) + last_item = formatted_items[-1] + return ("{prefix_plural}{first_items} {conjunction} {last_item}").format( + **locals() + ) + + +def formatted_list( + items, + conjunction="and", + before_each="‘", + after_each="’", + separator=", ", + prefix="", + prefix_plural="", +): + return Markup( + unescaped_formatted_list( + [escape_html(x) for x in items], + conjunction, + before_each, + after_each, + separator, + prefix, + prefix_plural, + ) + ) + + +def remove_whitespace_before_punctuation(value): + return re.sub(whitespace_before_punctuation, lambda match: match.group(1), value) + + +def make_quotes_smart(value): + return smartypants.smartypants(value, smartypants.Attr.q | smartypants.Attr.u) + + +def replace_hyphens_with_en_dashes(value): + return re.sub( + hyphens_surrounded_by_spaces, + (" " "\u2013" " "), # space # en dash # space + value, + ) + + +def replace_hyphens_with_non_breaking_hyphens(value): + return value.replace( + "-", + "\u2011", # non-breaking hyphen + ) + + +def normalise_whitespace_and_newlines(value): + return "\n".join(get_lines_with_normalised_whitespace(value)) + + +def get_lines_with_normalised_whitespace(value): + return [normalise_whitespace(line) for line in value.splitlines()] + + +def normalise_whitespace(value): + # leading and trailing whitespace removed + # inner whitespace with width becomes a single space + # inner whitespace with zero width is removed + # multiple space characters next to each other become just a single space character + for character in OBSCURE_FULL_WIDTH_WHITESPACE: + value = value.replace(character, " ") + + for character in OBSCURE_ZERO_WIDTH_WHITESPACE: + value = value.replace(character, "") + + return " ".join(value.split()) + + +def normalise_multiple_newlines(value): + return more_than_two_newlines_in_a_row.sub("\n\n", value) + + +def strip_leading_whitespace(value): + return value.lstrip() + + +def add_trailing_newline(value): + return "{}\n".format(value) + + +def remove_smart_quotes_from_email_addresses(value): + def remove_smart_quotes(match): + value = match.group(0) + for character in "‘’": + value = value.replace(character, "'") + return value + + return email_with_smart_quotes_regex.sub( + remove_smart_quotes, + value, + ) + + +def strip_all_whitespace(value, extra_characters=""): + # Removes from the beginning and end of the string all whitespace characters and `extra_characters` + if value is not None and hasattr(value, "strip"): + return value.strip(ALL_WHITESPACE + extra_characters) + return value + + +def strip_and_remove_obscure_whitespace(value): + if value == "": + # Return early to avoid making multiple, slow calls to + # str.replace on an empty string + return "" + + for character in OBSCURE_ZERO_WIDTH_WHITESPACE + OBSCURE_FULL_WIDTH_WHITESPACE: + value = value.replace(character, "") + + return value.strip(string.whitespace) + + +def remove_whitespace(value): + # Removes ALL whitespace, not just the obscure characters we normaly remove + for character in ALL_WHITESPACE: + value = value.replace(character, "") + + return value + + +def strip_unsupported_characters(value): + return value.replace("\u2028", "") diff --git a/notifications_utils/insensitive_dict.py b/notifications_utils/insensitive_dict.py new file mode 100644 index 000000000..7b239b884 --- /dev/null +++ b/notifications_utils/insensitive_dict.py @@ -0,0 +1,59 @@ +from functools import lru_cache + +from ordered_set import OrderedSet + + +class InsensitiveDict(dict): + """ + `InsensitiveDict` behaves like an ordered dictionary, except it normalises + case, whitespace, hypens and underscores in keys. + + In other words, + InsensitiveDict({'FIRST_NAME': 'example'}) == InsensitiveDict({'first name': 'example'}) + >>> True + """ + + KEY_TRANSLATION_TABLE = {ord(c): None for c in " _-"} + + def __init__(self, row_dict): + for key, value in row_dict.items(): + self[key] = value + + @classmethod + def from_keys(cls, keys): + """ + This behaves like `dict.from_keys`, except: + - it normalises the keys to ignore case, whitespace, hypens and + underscores + - it stores the original, unnormalised key as the value of the + item so it can be retrieved later + """ + return cls({key: key for key in keys}) + + def keys(self): + return OrderedSet(super().keys()) + + def __getitem__(self, key): + return super().__getitem__(self.make_key(key)) + + def __setitem__(self, key, value): + super().__setitem__(self.make_key(key), value) + + def __contains__(self, key): + return super().__contains__(self.make_key(key)) + + def get(self, key, default=None): + return self[key] if key in self else default + + def copy(self): + return self.__class__(super().copy()) + + def as_dict_with_keys(self, keys): + return {key: self.get(key) for key in keys} + + @staticmethod + @lru_cache(maxsize=32, typed=False) + def make_key(original_key): + if original_key is None: + return None + return original_key.translate(InsensitiveDict.KEY_TRANSLATION_TABLE).lower() diff --git a/notifications_utils/international_billing_rates.py b/notifications_utils/international_billing_rates.py new file mode 100644 index 000000000..f725834f4 --- /dev/null +++ b/notifications_utils/international_billing_rates.py @@ -0,0 +1,31 @@ +""" +Format of the yaml file looks like: + +1: + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 1 + names: + - Canada + - United States + - Dominican Republic +""" + +import os + +import yaml + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open("{}/international_billing_rates.yml".format(dir_path)) as f: + INTERNATIONAL_BILLING_RATES = yaml.safe_load(f) + COUNTRY_PREFIXES = list( + reversed(sorted(INTERNATIONAL_BILLING_RATES.keys(), key=len)) + ) diff --git a/notifications_utils/international_billing_rates.yml b/notifications_utils/international_billing_rates.yml new file mode 100644 index 000000000..dfa87d490 --- /dev/null +++ b/notifications_utils/international_billing_rates.yml @@ -0,0 +1,2891 @@ +################################################# +# +# DO NOT MODIFY THE "attributes" IN THIS FILE +# +# This file was generated from an external source, +# so its content should be kept as-is to avoid any +# ambiguity if we regenerate it in future. If you +# find something is incorrect, add a comment. +# +# It's OK to modify the "billable_units", as these +# get used for our actual billing calculations. +# +################################################# +# +# Key for entries: +# +# *in all cases, "null" means "don't know" or "n/a" +# +# alpha: +# possible values: 'REG' | 'YES' | 'NO' +# description: whether alphanumeric sender names are supported, potentially by registration only ('REG') +# comment: +# possible values: +# description: additional usage info e.g. "OPT-OUT option in the message is required" +# dlr: +# possible_values: '' | 'YES' | 'Carrier DLR' | 'NO' +# description: whether we get delivery receipts; 'Carrier DLR' also means 'NO'; unclear what '' means +# generic_sender: +# possible_values: '' | +# description: supports unregistered senders by converted to ('' means we don't know what is) +# numeric: +# possible_values: 'NO' | 'YES' | 'LIMITED' +# description: whether numeric sender names are supported, or randomly generated ('LIMITED') +# sc: +# possible_values: 'NO' | 'YES' | 'LIMITED' | 'REG' +# description: whether short codes are supported, potentially by registration only ('REG'), potentially limited in availability ('LIMITED') +# sender_and_registration_info: +# possible_values: +# description: specific operational info e.g. "All senders CONVERTED into random long numeric senders" +# text_restrictions: +# possible_values: +# description: similar to "comment" + +'1': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 1 + names: + - Canada + - United States + - Dominican Republic +'7': + attributes: + alpha: REG + comment: HIGH FEEs for SPAM + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: Transactional traffic ONLY + billable_units: 1 + names: + - South Ossetia + - Kazakhstan + - Abkhazia + - Russian Federation +'20': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: null + numeric: REG + sc: REG + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Egypt +'27': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into long numeric sender + text_restrictions: null + billable_units: 1 + names: + - South Africa +'30': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: Senders MUST NOT include "," (comma separator) within, + up to 11 chars in length + text_restrictions: NO unicode nor binary formatted SMS support + billable_units: 2 + names: + - Greece +'31': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Netherlands +'32': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Belgium +'33': + attributes: + alpha: 'YES' + comment: STOP and CONTACT for OPT-OUTs required + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: REG + sender_and_registration_info: All numeric senders CONVERTED into limited amount + of registered shared SCs. HIGH one time and monthly FEEs for each additional + SC + text_restrictions: Marketing traffic is on hold on working days from 10PM to 8AM + (UTC/GMT +1 hour), weekend and bank holidays. Transactional traffic can be allowed with + no time limits with approval only. + billable_units: 2 + names: + - France +'34': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'YES' + sender_and_registration_info: All long numeric senders not starting with 34 are + CONVERTED to "InfoSMS" + text_restrictions: null + billable_units: 2 + names: + - Spain +'36': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED to one national long numeric + text_restrictions: null + billable_units: 3 + names: + - Hungary +'39': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: Numeric national (local) sender IDs allowed only + text_restrictions: null + billable_units: 2 + names: + - Italy +'40': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: REG + sender_and_registration_info: All not registered senders are CONVERTED into one + SC. Monthly time FEE for each SC, one time fee for each Alpha sender, authorization + letter and description required. Unregistered senders are converted to SC 1797 + text_restrictions: null + billable_units: 2 + names: + - Romania +'41': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'YES' + sender_and_registration_info: National numeric senders are not allowed + text_restrictions: null + billable_units: 2 + names: + - Switzerland +'43': + attributes: + alpha: REG + comment: OPT-IN REQUIRED for each end user + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: HIGH monthly FEE for alpha senders. Otherwise ONLY + one long numeric sender allowed + text_restrictions: Bulk traffic NOT allowed. NO political content and other text + restrictions + billable_units: 3 + names: + - Austria +'44': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Guernsey + - Isle of Man + - Jersey +'45': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: '' + text_restrictions: Only transactional traffic allowed + billable_units: 1 + names: + - Denmark +'46': + attributes: + alpha: 'YES' + comment: HIGH FEEs for SPAM + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: '' + text_restrictions: '' + billable_units: 2 + names: + - Sweden +'47': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Norway +'48': + attributes: + alpha: 'YES' + comment: Extremely HIGH penalties for marketing messages without OPT-Ins + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: '' + billable_units: 1 + names: + - Poland +'49': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Germany +'51': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: LIMITED + sender_and_registration_info: All senders CONVERTED into one SC + text_restrictions: null + billable_units: 2 + names: + - Peru +'52': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 2 + names: + - Mexico +'53': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Cuba +'54': + attributes: + alpha: 'NO' + comment: OPT-IN REQUIRED for each end user + dlr: 'NO' + generic_sender: '' + numeric: 'NO' + sc: LIMITED + sender_and_registration_info: All senders CONVERTED into one available shared + SC + text_restrictions: Political content NOT allowed + billable_units: 3 + names: + - Argentina +'55': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: LIMITED + sender_and_registration_info: LIMITED amount of SCs available + text_restrictions: NO marketing traffic. NO special characters. 160 chars per + message available + billable_units: 1 + names: + - Brazil +'56': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 2 + names: + - Chile +'57': + attributes: + alpha: 'NO' + comment: null + dlr: 'NO' + generic_sender: '' + numeric: 'NO' + sc: LIMITED + sender_and_registration_info: LIMITED amount of SCs available + text_restrictions: null + billable_units: 1 + names: + - Colombia +'58': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Venezuela +'60': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: REG + sender_and_registration_info: LIMITED amount of registered shared SCs available. + HIGH one time and monthly FEEs for each additional dedicated SC + text_restrictions: null + billable_units: 1 + names: + - Malaysia +'61': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: Long numeric MUST NOT begin with "0" (zero) + text_restrictions: null + billable_units: 2 + names: + - Australia +'62': + attributes: + alpha: REG + comment: Local clients NOT allowed. International clients ONLY + dlr: 'YES' + generic_sender: globalsms/InfoSMS + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: '' + billable_units: 1 + names: + - Indonesia +'63': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: INFO / Globalsms + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: All numeric senders are converted to InfoText + text_restrictions: Adult, alcohol, drugs, gambling, election and tobacco contents + are strictly forbidden + billable_units: 1 + names: + - Philippines +'64': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: All alpha senders converted to a random UK longnumber + text_restrictions: null + billable_units: 3 + names: + - New Zealand +'65': + attributes: + alpha: REG + comment: Where SMS exceeds 160 characters, it shall be broken into two or more + messages and transmitted separately + dlr: 'YES' + generic_sender: InfoSMS + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All not registered senders are CONVERTED into "InfoSMS". + LIMITED amount of free senders, MONTHLY FEE for additional Alpha and numeric + senders. Only national long numeric senders ara available + text_restrictions: '' + billable_units: 1 + names: + - Singapore +'66': + attributes: + alpha: REG + comment: Maximum long message lenght is 459 for GSM7 or 201 for Unicode alphabet. + Special registration procedure for sending to DND numbers + dlr: 'YES' + generic_sender: SMS + numeric: REG + sc: REG + sender_and_registration_info: Alpha sender up to 11 characters in length. NO " + " (space) support in the sender name. NO special character at the beginning + of the sender. Numeric sender up to 11 digits in length. Dynamic sender available + over Offnet connection. + text_restrictions: NO political nor erotic content and other text restrictions + billable_units: 1 + names: + - Thailand +'81': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: 11-digit long short code. + text_restrictions: null + billable_units: 3 + names: + - Japan +'82': + attributes: + alpha: 'NO' + comment: Message length - 140 characters + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: 'All Alpha senders CONVERTED to one local long numeric. + "00" is ADDED in front of international long numeric sender ids. ' + text_restrictions: "[\uAD6D\uC81C\uBC1C\uC2E0] is added in front of SMS text for\ + \ every inbound P2P and A2P coming from overseas countries." + billable_units: 2 + names: + - Korea, Republic of +'84': + attributes: + alpha: REG + comment: null + dlr: Carrier DLR + generic_sender: InfoSMS + numeric: 'NO' + sc: REG + sender_and_registration_info: One time and monthly FEEs for each sender. All not + registered senders CONVERTED into "InfoSMS" sender + text_restrictions: null + billable_units: 2 + names: + - Vietnam +'86': + attributes: + alpha: 'NO' + comment: Extremenly HIGH penalties for any traffic other than transactional + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into one available national + numeric sender + text_restrictions: Content template MUST be approved by the MNO. Transactional + traffic ONLY. Sufix added in the message text + billable_units: 1 + names: + - China +'90': + attributes: + alpha: REG + comment: ONLY for Turkish clients. International clients NOT allowed, unless approved + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: LIMITED + sender_and_registration_info: LIMITED amount of numeric senders are allowed (just + some ranges) + text_restrictions: NO lottery, gambling nor erotic content and other text restrictions + billable_units: 1 + names: + - Turkey + - Northern Cyprus +'91': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Alpha senders with exactly 6 characters ONLY + text_restrictions: Transactional traffic ONLY + billable_units: 1 + names: + - India +'92': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Pakistan +'93': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: SMS-Info + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: 'All not registered senders CONVERTED to ''SMS-Info''. + Sender and text example required prior to registration. Registration ETA: up + to 10 days.' + text_restrictions: null + billable_units: 3 + names: + - Afghanistan +'94': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Sri Lanka +'95': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: There is monthly FEE for renting a SC. No generic + senders available. WEB page and description for each sender needed. + text_restrictions: null + billable_units: 2 + names: + - Myanmar +'98': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Iran +'211': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - South Sudan +'212': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: Globalsms + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Case sensitive senders + text_restrictions: null + billable_units: 2 + names: + - Morocco +'213': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: InfoSMS/SMS + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Algeria +'216': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Tunisia +'218': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Libya +'220': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Gambia +'221': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Senegal +'222': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Mauritania +'223': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Mali +'224': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: 00 added in front of destination + text_restrictions: null + billable_units: 3 + names: + - Guinea +'225': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Cote d'Ivoire +'226': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Burkina Faso +'227': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Niger +'228': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Togo +'229': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Benin +'230': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Mauritius +'231': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Liberia +'232': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Sierra Leone +'233': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: REG + sender_and_registration_info: One time and monthly FEEs for each SC + text_restrictions: null + billable_units: 1 + names: + - Ghana +'234': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: SMS + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Nigeria +'235': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Chad +'236': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Central African Republic +'237': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Cameroon +'238': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Cape Verde +'239': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Sao Tome and Principe +'240': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Equatorial Guinea +'241': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Gabon +'242': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Congo +'243': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Congo, Democratic Republic of +'244': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: Info + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Angola +'245': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Guinea-Bissau +'246': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - British Indian Ocean Territory +'248': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Seychelles +'249': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Sudan +'250': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Rwanda, Republic of +'251': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Ethiopia +'252': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Somalia +'253': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Djibouti, Republic of +'254': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: ONLY one shared SC available. HIGH one time and + monthly FEEs for each additional sender. Only local entities can register senders + for Safaricom. + text_restrictions: null + billable_units: 1 + names: + - Kenya +'255': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Tanzania +'256': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: INFOSMS + numeric: 'NO' + sc: REG + sender_and_registration_info: All not registered senders CONVERTED to "INFOSMS". + Only local entities can register the sender (requires Authorization letter). + text_restrictions: As per regulation, 'DND*196#' is being added at the end of + each message. + billable_units: 1 + names: + - Uganda +'257': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Burundi +'258': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Mozambique +'260': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Zambia +'261': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Madagascar +'262': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Reunion +'263': + attributes: + alpha: 'YES' + comment: null + dlr: Carrier DLR + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Zimbabwe +'264': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Long registration procedure, up to 30 days. Monthly + FEE for each sender + text_restrictions: null + billable_units: 1 + names: + - Namibia +'265': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Malawi +'266': + attributes: + alpha: null # should be 'REG' + comment: null + dlr: null # should be 'YES' + generic_sender: null # should be "''" + numeric: null # should be 'NO' + sc: null # should be 'NO' + sender_and_registration_info: null # should be "Sender names can only be 3-11 chars, no spaces" + text_restrictions: null + billable_units: 3 + names: + - Lesotho +'267': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Botswana +'268': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Swaziland +'269': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Comoros +'297': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Aruba +'298': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Faroe Islands +'299': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Greenland +'350': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Gibraltar +'351': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Portugal +'352': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Luxembourg +'353': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: Numeric sender up to 12 digits in length. SC available + upon registration + text_restrictions: null + billable_units: 2 + names: + - Ireland +'354': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Iceland +'355': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Albania +'356': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Malta +'357': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: InfoSMS/Message + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Senders up to 11 characters in length. NO special + characters. Generic senders available + text_restrictions: NO violent, offensive, discriminatory or erotic content + billable_units: 1 + names: + - Cyprus +'358': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: $ INSERTED in front of each Alpha sender, "00" in + front of long numeric senders + text_restrictions: null + billable_units: 2 + names: + - Finland +'359': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: Sender converts to short code 1917. + text_restrictions: null + billable_units: 3 + names: + - Bulgaria +'370': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Lithuania +'371': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Latvia +'372': + attributes: + alpha: 'YES' + comment: OPT-INs required + dlr: 'YES' + generic_sender: '' + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: '' + text_restrictions: NO messages in any way related to premium rate services + billable_units: 2 + names: + - Estonia +'373': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: All senders CONVERTED into one SC + text_restrictions: null + billable_units: 3 + names: + - Moldova +'374': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: REG + sender_and_registration_info: NO long numeric senders + text_restrictions: null + billable_units: 3 + names: + - Armenia +'375': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: SMSinfo + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Belarus +'376': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Andorra +'377': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Monaco +'378': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - San Marino, Republic of +'380': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: Info/INFO/InfoSMS/SMS + numeric: REG + sc: 'NO' + sender_and_registration_info: Alpha senders are case sensitive + text_restrictions: null + billable_units: 3 + names: + - Ukraine +'381': + attributes: + alpha: REG + comment: All traffic bulks MUST be registered at MNO + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: REG + sender_and_registration_info: Monthly FEE for each SC + text_restrictions: Transactional traffic ONLY + billable_units: 1 + names: + - Serbia +'382': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: null + numeric: REG + sc: REG + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Montenegro +'385': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: InfoSMS + numeric: 'NO' + sc: REG + sender_and_registration_info: One time and monthly FEES for each sender. NO special + chars nor " " (space) support in the sender name. Instead of space "_" (underscore) + is being used + text_restrictions: null + billable_units: 2 + names: + - Croatia +'386': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: InfoSMS + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Slovenia +'387': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: INFOSMS + numeric: REG + sc: REG + sender_and_registration_info: Numeric sender available for the FEE upon registration. + SC registration CHARGED - one time and monthly FEEs per each sender. + text_restrictions: null + billable_units: 1 + names: + - Bosnia and Herzegovina +'389': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Macedonia +'420': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: Info + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: All not registered senders CONVERTED into "Info" + sender. Monthly FEE for each additional sender. NO special chars nor " " (space) + support in the sender name + text_restrictions: Traffic allowed ONLY between 8am and 6pm (CET) on working days. + Content promoting lottery, betting, gambling nor consumer loans NOT allowed. + NO political, violent, erotic nor abusive content and other text restrictions + billable_units: 2 + names: + - Czech Republic +'421': + attributes: + alpha: 'YES' + comment: P2P not allowed + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: 'NO' + sender_and_registration_info: SC up to 6 digits in length. One time and monthly + FEEs for numeric senders. All not registered long numeric senders are CONVERTED + into "Info" + text_restrictions: '' + billable_units: 2 + names: + - Slovakia +'423': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: LIMITED + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Liechtenstein +'500': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Falkland Islands +'501': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Belize +'502': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Guatemala +'503': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - El Salvador +'504': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Honduras +'505': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Nicaragua +'506': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Costa Rica +'507': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Panama +'508': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Saint Pierre and Miquelon +'509': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Haiti +'590': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Guadeloupe +'591': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Bolivia +'592': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Guyana +'593': + attributes: + alpha: 'NO' + comment: null + dlr: '' + generic_sender: null + numeric: 'NO' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Ecuador +'594': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - French Guiana +'595': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Paraguay +'596': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Martinique +'597': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Suriname +'598': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Uruguay +'599': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Curacao (former Netherlands Antilles) +'670': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Timor L'este +'672': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Norfolk Island +'673': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Brunei Darussalam +'674': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Nauru +'675': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Papua New Guinea +'676': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Tonga +'677': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Solomon Islands +'678': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Vanuatu +'679': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Fiji +'680': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Palau +'682': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Cook Islands +'685': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Samoa +'687': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - New Caledonia +'689': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - French Polynesia +'691': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Micronesia, Federated States of +'692': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Marshall Islands +'852': + attributes: + alpha: REG + comment: DND register is being used + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: OPT-IN required for promotional traffic. NO violent, discriminatory + nor erotic content + billable_units: 3 + names: + - Hong Kong +'853': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Macau +'855': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: null + billable_units: 1 + names: + - Cambodia +'856': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Laos +'880': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Bangladesh +'886': + attributes: + alpha: 'NO' + comment: null + dlr: 'NO' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: ONLY one shared long numeric available + text_restrictions: null + billable_units: 2 + names: + - Taiwan +'960': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Maldives +'961': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Lebanon +'962': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Jordan +'963': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Syria +'964': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 3 + names: + - Iraq +'965': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: No new senders available at the moment. Registration + ETA 1-5 days. + text_restrictions: null + billable_units: 2 + names: + - Kuwait +'966': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Saudi Arabia +'967': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Yemen +'968': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Only local entities can register senders. NOC and + Trace Licence required. + text_restrictions: null + billable_units: 1 + names: + - Oman +'970': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Palestinian Territory +'971': + attributes: + alpha: REG + comment: OPT-OUT option in the message is required + dlr: 'YES' + generic_sender: SMS-Info + numeric: 'NO' + sc: REG + sender_and_registration_info: NOC letter is required to begin the registration + process. One time and monthly FEEs for each additional SC + text_restrictions: 'No marketing traffic between 8pm and 8am (GMT +4). International + traffic is allowed. ' + billable_units: 2 + names: + - United Arab Emirates +'972': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Israel +'973': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Bahrain +'974': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: INFOSMSI / Message + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Case sensitive senders. SCs and numeric senders + MUST have more than 5 digits. All not registered senders are CONVERTED to "INFOSMSI" + text_restrictions: null + billable_units: 1 + names: + - Qatar +'975': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Bhutan +'976': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Mongolia +'977': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'NO' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Nepal +'992': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: 'NO' + sender_and_registration_info: Numeric sender available upon registration. Numeric + sender MUST begin with "992" or "0" (zero) + text_restrictions: null + billable_units: 1 + names: + - Tajikistan +'993': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Turkmenistan +'994': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: NO political, erotic, religious, alcoholic or tobacco products, + premium services related info. APPROVAL required for food add., medical, non-govern. + organizations or minor individuals related messages + billable_units: 3 + names: + - Azerbaijan +'995': + attributes: + alpha: REG + comment: null + dlr: 'YES' + generic_sender: InfoSMS + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: '' + text_restrictions: Few GSM 7 characters are not supported + billable_units: 1 + names: + - Georgia +'996': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: REG + sc: 'NO' + sender_and_registration_info: ONLY two shared numeric senders available. Long + numeric usage MUST be approved by MNO + text_restrictions: null + billable_units: 1 + names: + - Kyrgyzstan +'998': + attributes: + alpha: REG + comment: All features are supported (DLRs, longSMS, special characters, Unicode) + dlr: 'YES' + generic_sender: InfoSMS + numeric: 'NO' + sc: 'NO' + sender_and_registration_info: Numeric and all non registered senders are converted + to InfoSMS + text_restrictions: '' + billable_units: 1 + names: + - Uzbekistan +'1242': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 2 + names: + - Bahamas +'1246': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 2 + names: + - Barbados +'1264': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Anguilla +'1268': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Antigua and Barbuda +'1284': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Virgin Islands, British +'1345': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Cayman Islands +'1441': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Bermuda +'1473': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Grenada +'1649': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Turks and Caicos Islands +'1664': + attributes: + alpha: 'NO' + comment: null + dlr: 'YES' + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 1 + names: + - Montserrat +'1684': + attributes: + alpha: 'NO' + comment: null + dlr: Carrier DLR + generic_sender: '' + numeric: LIMITED + sc: 'NO' + sender_and_registration_info: All senders CONVERTED into random long numeric senders + text_restrictions: Bulk/marketing traffic NOT allowed + billable_units: 3 + names: + - American Samoa +'1721': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Sint Maarten +'1758': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Saint Lucia +'1767': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Dominica, Commonwealth of +'1784': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Saint Vincent and The Grenadines +'1868': + attributes: + alpha: null + comment: null + dlr: null + generic_sender: null + numeric: null + sc: null + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Trinidad and Tobago +'1869': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 2 + names: + - Saint Kitts and Nevis +'1876': + attributes: + alpha: 'YES' + comment: null + dlr: 'YES' + generic_sender: null + numeric: 'YES' + sc: 'YES' + sender_and_registration_info: null + text_restrictions: null + billable_units: 1 + names: + - Jamaica diff --git a/notifications_utils/jinja_templates/broadcast_preview_template.jinja2 b/notifications_utils/jinja_templates/broadcast_preview_template.jinja2 new file mode 100644 index 000000000..bf22045fe --- /dev/null +++ b/notifications_utils/jinja_templates/broadcast_preview_template.jinja2 @@ -0,0 +1,12 @@ +
+

+ + Emergency alert +

+ {{ body }} +
diff --git a/notifications_utils/jinja_templates/email_preview_template.jinja2 b/notifications_utils/jinja_templates/email_preview_template.jinja2 new file mode 100644 index 000000000..68314f203 --- /dev/null +++ b/notifications_utils/jinja_templates/email_preview_template.jinja2 @@ -0,0 +1,39 @@ + diff --git a/notifications_utils/jinja_templates/email_template.jinja2 b/notifications_utils/jinja_templates/email_template.jinja2 new file mode 100644 index 000000000..5ecd58a8d --- /dev/null +++ b/notifications_utils/jinja_templates/email_template.jinja2 @@ -0,0 +1,252 @@ +{% if complete_html %} + + + + + + + + {{ subject }} + + + + + + + + +{% endif %} +{{ preheader }}â€Ļ +{% if govuk_banner %} + + + + +
+ + + + + +
+ + + + + +
+ Notify.gov +
+
+
+ +
+ + + + + + + +{% endif %} +{% if brand_banner %} + {% set brand_colour = brand_colour if brand_colour else '#0b0c0c' %} + + + + +
+ + + + {% if brand_logo %} + + {% endif %} + {% if brand_text %} + + {% endif %} + +
+ {% if brand_text %}{% else -%}{{ brand_name }}{%- endif %} + + + {{ brand_text }} + +
+ +
+{% endif %} +{% if brand_logo and not brand_banner %} + + + + + + + +{% endif %} + + + + + + + + + + + + + +{% if complete_html %} + + +{% endif %} diff --git a/notifications_utils/jinja_templates/letter_image_template.jinja2 b/notifications_utils/jinja_templates/letter_image_template.jinja2 new file mode 100644 index 000000000..b03428ca1 --- /dev/null +++ b/notifications_utils/jinja_templates/letter_image_template.jinja2 @@ -0,0 +1,37 @@ +{% for page_number in page_numbers %} +
+ {% if loop.first and show_postage %} +

+ Postage: {{ postage_description }} +

+ {% endif %} + +
+{% endfor %} + +
+

+ Recipient address +

+
    + {%- for line in address -%} +
  • {{ line }}
  • + {%- endfor -%} +
+

+ Contact block +

+

+ {{ contact_block }} +

+

+ Content +

+

+ {{ date }} +

+

+ {{ subject }} +

+ {{ message }} +
diff --git a/notifications_utils/jinja_templates/letter_pdf/_body.jinja2 b/notifications_utils/jinja_templates/letter_pdf/_body.jinja2 new file mode 100644 index 000000000..6e16bfe61 --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/_body.jinja2 @@ -0,0 +1,33 @@ + + + +
+
+ 000_000_0000000_000000_0000_00000 +
+
    + {%- for line in address -%} +
  • {{ line }}
  • + {%- endfor -%} +
+
+
+
+
+
+
+ {{ contact_block }} +
+
+

+ {{ date }} +

+

{{ subject }}

+ {{ message }} +
+ + diff --git a/notifications_utils/jinja_templates/letter_pdf/_head.jinja2 b/notifications_utils/jinja_templates/letter_pdf/_head.jinja2 new file mode 100644 index 000000000..064237fa6 --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/_head.jinja2 @@ -0,0 +1,7 @@ + + + + + + Preview – GOV.UK Notify + diff --git a/notifications_utils/jinja_templates/letter_pdf/_main_css.jinja2 b/notifications_utils/jinja_templates/letter_pdf/_main_css.jinja2 new file mode 100644 index 000000000..c73684d32 --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/_main_css.jinja2 @@ -0,0 +1,197 @@ +{% set line_height = '16.0pt' %} + diff --git a/notifications_utils/jinja_templates/letter_pdf/_print_only_css.jinja2 b/notifications_utils/jinja_templates/letter_pdf/_print_only_css.jinja2 new file mode 100644 index 000000000..3d4c2c59b --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/_print_only_css.jinja2 @@ -0,0 +1,17 @@ + diff --git a/notifications_utils/jinja_templates/letter_pdf/preview.jinja2 b/notifications_utils/jinja_templates/letter_pdf/preview.jinja2 new file mode 100644 index 000000000..d1b73fefb --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/preview.jinja2 @@ -0,0 +1,3 @@ +{% include 'letter_pdf/_head.jinja2' %} +{% include 'letter_pdf/_main_css.jinja2' %} +{% include 'letter_pdf/_body.jinja2' %} diff --git a/notifications_utils/jinja_templates/letter_pdf/print.jinja2 b/notifications_utils/jinja_templates/letter_pdf/print.jinja2 new file mode 100644 index 000000000..f56f49746 --- /dev/null +++ b/notifications_utils/jinja_templates/letter_pdf/print.jinja2 @@ -0,0 +1,4 @@ +{% include 'letter_pdf/_head.jinja2' %} +{% include 'letter_pdf/_main_css.jinja2' %} +{% include 'letter_pdf/_print_only_css.jinja2' %} +{% include 'letter_pdf/_body.jinja2' %} diff --git a/notifications_utils/jinja_templates/sms_preview_template.jinja2 b/notifications_utils/jinja_templates/sms_preview_template.jinja2 new file mode 100644 index 000000000..dea33cce5 --- /dev/null +++ b/notifications_utils/jinja_templates/sms_preview_template.jinja2 @@ -0,0 +1,13 @@ +{% if show_sender %} +

+ From: {{ sender }} +

+{% endif %} +{% if show_recipient %} +

+ To: {{ recipient }} +

+{% endif %} +
+ {{ body }} +
diff --git a/notifications_utils/letter_timings.py b/notifications_utils/letter_timings.py new file mode 100644 index 000000000..62abf2c21 --- /dev/null +++ b/notifications_utils/letter_timings.py @@ -0,0 +1,180 @@ +from collections import namedtuple +from datetime import datetime, time, timedelta + +import pytz +from govuk_bank_holidays.bank_holidays import BankHolidays + +from notifications_utils.countries.data import Postage +from notifications_utils.timezones import utc_string_to_aware_gmt_datetime + +LETTER_PROCESSING_DEADLINE = time(17, 30) +CANCELLABLE_JOB_LETTER_STATUSES = [ + "created", + "cancelled", + "virus-scan-failed", + "validation-failed", + "technical-failure", + "pending-virus-check", +] + + +non_working_days_dvla = BankHolidays( + use_cached_holidays=True, + weekend=(5, 6), +) +non_working_days_royal_mail = BankHolidays( + use_cached_holidays=True, + weekend=(6,), # Only Sunday (day 6 of the week) is a non-working day +) + + +def set_gmt_hour(day, hour): + return ( + day.astimezone(pytz.timezone("Europe/London")) + .replace(hour=hour, minute=0) + .astimezone(pytz.utc) + ) + + +def get_next_work_day(date, non_working_days): + next_day = date + timedelta(days=1) + if non_working_days.is_work_day( + date=next_day.date(), + division=BankHolidays.ENGLAND_AND_WALES, + ): + return next_day + return get_next_work_day(next_day, non_working_days) + + +def get_next_dvla_working_day(date): + """ + Printing takes place monday to friday, excluding bank holidays + """ + return get_next_work_day(date, non_working_days=non_working_days_dvla) + + +def get_next_royal_mail_working_day(date): + """ + Royal mail deliver letters on monday to saturday + """ + return get_next_work_day(date, non_working_days=non_working_days_royal_mail) + + +def get_delivery_day(date, *, days_to_deliver): + next_day = get_next_royal_mail_working_day(date) + if days_to_deliver == 1: + return next_day + return get_delivery_day(next_day, days_to_deliver=(days_to_deliver - 1)) + + +def get_min_and_max_days_in_transit(postage): + return { + # first class post is printed earlier in the day, so will + # actually transit on the printing day, and be delivered the next + # day, so effectively spends no full days in transit + "first": (0, 0), + "second": (1, 2), + Postage.EUROPE: (3, 5), + Postage.REST_OF_WORLD: (5, 7), + }[postage] + + +def get_earliest_and_latest_delivery(print_day, postage): + for days_to_transit in get_min_and_max_days_in_transit(postage): + yield get_delivery_day(print_day, days_to_deliver=1 + days_to_transit) + + +def get_letter_timings(upload_time, postage): + LetterTimings = namedtuple( + "LetterTimings", "printed_by, is_printed, earliest_delivery, latest_delivery" + ) + + # shift anything after 5:30pm to the next day + processing_day = utc_string_to_aware_gmt_datetime(upload_time) + timedelta( + hours=6, minutes=30 + ) + print_day = get_next_dvla_working_day(processing_day) + + earliest_delivery, latest_delivery = get_earliest_and_latest_delivery( + print_day, postage + ) + + # print deadline is 3pm BST + printed_by = set_gmt_hour(print_day, hour=15) + now = ( + datetime.utcnow() + .replace(tzinfo=pytz.utc) + .astimezone(pytz.timezone("Europe/London")) + ) + + return LetterTimings( + printed_by=printed_by, + is_printed=(now > printed_by), + earliest_delivery=set_gmt_hour(earliest_delivery, hour=16), + latest_delivery=set_gmt_hour(latest_delivery, hour=16), + ) + + +def letter_can_be_cancelled(notification_status, notification_created_at): + """ + If letter does not have status of created or pending-virus-check + => can't be cancelled (it has already been processed) + + If it's after 5.30pm local time and the notification was created today before 5.30pm local time + => can't be cancelled (it will already be zipped up to be sent) + """ + if notification_status not in ("created", "pending-virus-check"): + return False + + if too_late_to_cancel_letter(notification_created_at): + return False + return True + + +def too_late_to_cancel_letter(notification_created_at): + time_created_at = notification_created_at + day_created_on = time_created_at.date() + + current_time = datetime.utcnow() + current_day = current_time.date() + if ( + _after_letter_processing_deadline() + and _notification_created_before_today_deadline(notification_created_at) + ): + return True + if ( + _notification_created_before_that_day_deadline(notification_created_at) + and day_created_on < current_day + ): + return True + if (current_day - day_created_on).days > 1: + return True + + +def _after_letter_processing_deadline(): + current_utc_datetime = datetime.utcnow() + bst_time = current_utc_datetime.time() + + return bst_time >= LETTER_PROCESSING_DEADLINE + + +def _notification_created_before_today_deadline(notification_created_at): + current_bst_datetime = datetime.utcnow() + todays_deadline = current_bst_datetime.replace( + hour=LETTER_PROCESSING_DEADLINE.hour, + minute=LETTER_PROCESSING_DEADLINE.minute, + ) + + notification_created_at_in_bst = notification_created_at + + return notification_created_at_in_bst <= todays_deadline + + +def _notification_created_before_that_day_deadline(notification_created_at): + notification_created_at_bst_datetime = notification_created_at + created_at_day_deadline = notification_created_at_bst_datetime.replace( + hour=LETTER_PROCESSING_DEADLINE.hour, + minute=LETTER_PROCESSING_DEADLINE.minute, + ) + + return notification_created_at_bst_datetime <= created_at_day_deadline diff --git a/notifications_utils/logging.py b/notifications_utils/logging.py new file mode 100644 index 000000000..6a209cdd3 --- /dev/null +++ b/notifications_utils/logging.py @@ -0,0 +1,133 @@ +import logging +import logging.handlers +import sys +from itertools import product + +from flask import g, request +from flask.ctx import has_app_context, has_request_context +from flask.logging import default_handler +from pythonjsonlogger.jsonlogger import JsonFormatter as BaseJSONFormatter + +LOG_FORMAT = ( + "%(asctime)s %(app_name)s %(name)s %(levelname)s " + '%(request_id)s %(service_id)s "%(message)s" [in %(pathname)s:%(lineno)d]' +) +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" + +logger = logging.getLogger(__name__) + + +def init_app(app): + app.config.setdefault("NOTIFY_LOG_LEVEL", "INFO") + app.config.setdefault("NOTIFY_APP_NAME", "none") + + app.logger.removeHandler(default_handler) + + handlers = get_handlers(app) + loglevel = logging.getLevelName(app.config["NOTIFY_LOG_LEVEL"]) + loggers = [ + app.logger, + logging.getLogger("utils"), + logging.getLogger("notifications_python_client"), + logging.getLogger("werkzeug"), + ] + for logger_instance, handler in product(loggers, handlers): + logger_instance.addHandler(handler) + logger_instance.setLevel(loglevel) + warning_loggers = [logging.getLogger("boto3"), logging.getLogger("s3transfer")] + for logger_instance, handler in product(warning_loggers, handlers): + logger_instance.addHandler(handler) + logger_instance.setLevel(logging.WARNING) + app.logger.info("Logging configured") + + +def get_handlers(app): + handlers = [] + standard_formatter = logging.Formatter(LOG_FORMAT, TIME_FORMAT) + json_formatter = JSONFormatter(LOG_FORMAT, TIME_FORMAT) + + stream_handler = logging.StreamHandler(sys.stdout) + if not app.debug: + handlers.append(configure_handler(stream_handler, app, json_formatter)) + else: + # turn off 200 OK static logs in development + def is_200_static_log(log): + msg = log.getMessage() + return not ("GET /static/" in msg and " 200 " in msg) + + logging.getLogger("werkzeug").addFilter(is_200_static_log) + + # human readable stdout logs + handlers.append(configure_handler(stream_handler, app, standard_formatter)) + + return handlers + + +def configure_handler(handler, app, formatter): + handler.setLevel(logging.getLevelName(app.config["NOTIFY_LOG_LEVEL"])) + handler.setFormatter(formatter) + handler.addFilter(AppNameFilter(app.config["NOTIFY_APP_NAME"])) + handler.addFilter(RequestIdFilter()) + handler.addFilter(ServiceIdFilter()) + + return handler + + +class AppNameFilter(logging.Filter): + def __init__(self, app_name): + self.app_name = app_name + + def filter(self, record): + record.app_name = self.app_name + + return record + + +class RequestIdFilter(logging.Filter): + @property + def request_id(self): + default = "no-request-id" + if has_request_context() and hasattr(request, "request_id"): + return request.request_id or default + elif has_app_context() and "request_id" in g: + return g.request_id or default + else: + return default + + def filter(self, record): + record.request_id = self.request_id + + return record + + +class ServiceIdFilter(logging.Filter): + @property + def service_id(self): + default = "no-service-id" + if has_app_context() and "service_id" in g: + return g.service_id or default + else: + return default + + def filter(self, record): + record.service_id = self.service_id + + return record + + +class JSONFormatter(BaseJSONFormatter): + def process_log_record(self, log_record): + rename_map = { + "asctime": "time", + "request_id": "requestId", + "app_name": "application", + "service_id": "service_id", + } + for key, newkey in rename_map.items(): + log_record[newkey] = log_record.pop(key) + log_record["logType"] = "application" + try: + log_record["message"] = log_record["message"].format(**log_record) + except (KeyError, IndexError) as e: + logger.exception("failed to format log message: {} not found".format(e)) + return log_record diff --git a/notifications_utils/markdown.py b/notifications_utils/markdown.py new file mode 100644 index 000000000..7d2c719e1 --- /dev/null +++ b/notifications_utils/markdown.py @@ -0,0 +1,308 @@ +import re +from itertools import count + +import mistune +from ordered_set import OrderedSet + +from notifications_utils import MAGIC_SEQUENCE, magic_sequence_regex +from notifications_utils.formatters import create_sanitised_html_for_url + +LINK_STYLE = "word-wrap: break-word; color: #1D70B8;" + +mistune._block_quote_leading_pattern = re.compile(r"^ *\^ ?", flags=re.M) +mistune.BlockGrammar.block_quote = re.compile(r"^( *\^[^\n]+(\n[^\n]+)*\n*)+") +mistune.BlockGrammar.list_block = re.compile( + r"^( *)([â€ĸ*-]|\d+\.)[\s\S]+?" + r"(?:" + r"\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))" # hrule + r"|\n+(?=%s)" # def links + r"|\n+(?=%s)" # def footnotes + r"|\n{2,}" + r"(?! )" + r"(?!\1(?:[â€ĸ*-]|\d+\.) )\n*" + r"|" + r"\s*$)" + % ( + mistune._pure_pattern(mistune.BlockGrammar.def_links), + mistune._pure_pattern(mistune.BlockGrammar.def_footnotes), + ) +) +mistune.BlockGrammar.list_item = re.compile( + r"^(( *)(?:[â€ĸ*-]|\d+\.)[^\n]*" r"(?:\n(?!\2(?:[â€ĸ*-]|\d+\.))[^\n]*)*)", flags=re.M +) +mistune.BlockGrammar.list_bullet = re.compile(r"^ *(?:[â€ĸ*-]|\d+\.)") +mistune.InlineGrammar.url = re.compile(r"""^(https?:\/\/[^\s<]+[^<.,:"')\]\s])""") + +mistune.InlineLexer.default_rules = list( + OrderedSet(mistune.InlineLexer.default_rules) + - set( + ( + "emphasis", + "double_emphasis", + "strikethrough", + "code", + ) + ) +) +mistune.InlineLexer.inline_html_rules = list( + set(mistune.InlineLexer.inline_html_rules) + - set( + ( + "emphasis", + "double_emphasis", + "strikethrough", + "code", + ) + ) +) + + +class NotifyLetterMarkdownPreviewRenderer(mistune.Renderer): + # TODO if we start removing the dead code detected by + # the vulture tool (such as the parameter 'language' here) + # it will break all the tests. Need to do some massive + # cleanup apparently, although it's not clear why vulture + # only recently started detecting this. + def block_code(self, code, language=None): # noqa + return code + + def block_quote(self, text): + return text + + def header(self, text, level, raw=None): # noqa + if level == 1: + return super().header(text, 2) + return self.paragraph(text) + + def hrule(self): + return '
 
' + + def paragraph(self, text): + if text.strip(): + return "

{}

".format(text) + return "" + + def table(self, header, body): + return "" + + def autolink(self, link, is_email=False): + return "{}".format( + link.replace("http://", "").replace("https://", "") + ) + + def image(self, src, title, alt_text): # noqa + return "" + + def linebreak(self): + return "
" + + def newline(self): + return self.linebreak() + + def list_item(self, text): + return "
  • {}
  • \n".format(text.strip()) + + def link(self, link, title, content): + return "{}: {}".format(content, self.autolink(link)) + + def footnote_ref(self, key, index): + return "" + + def footnote_item(self, key, text): + return text + + def footnotes(self, text): + return text + + +class NotifyEmailMarkdownRenderer(NotifyLetterMarkdownPreviewRenderer): + def header(self, text, level, raw=None): # noqa + if level == 1: + return ( + '

    ' + "{}" + "

    " + ).format(text) + return self.paragraph(text) + + def hrule(self): + return '
    ' + + def linebreak(self): + return "
    " + + def list(self, body, ordered=True): + return ( + ( + '' + "" + '" + "" + "
    ' + '
      ' + "{}" + "
    " + "
    " + ).format(body) + if ordered + else ( + '' + "" + '" + "" + "
    ' + '
      ' + "{}" + "
    " + "
    " + ).format(body) + ) + + def list_item(self, text): + return ( + '
  • ' + "{}" + "
  • " + ).format(text.strip()) + + def paragraph(self, text): + if text.strip(): + return ( + '

    {}

    ' + ).format(text) + return "" + + def block_quote(self, text): + return ( + "
    " + "{}" + "
    " + ).format(text) + + def link(self, link, title, content): + return ('{}').format( + LINK_STYLE, + ' href="{}"'.format(link), + ' title="{}"'.format(title) if title else "", + content, + ) + + def autolink(self, link, is_email=False): + if is_email: + return link + return create_sanitised_html_for_url(link, style=LINK_STYLE) + + +class NotifyPlainTextEmailMarkdownRenderer(NotifyEmailMarkdownRenderer): + COLUMN_WIDTH = 65 + + def header(self, text, level, raw=None): # noqa + if level == 1: + return "".join( + ( + self.linebreak() * 3, + text, + self.linebreak(), + "-" * self.COLUMN_WIDTH, + ) + ) + return self.paragraph(text) + + def hrule(self): + return self.paragraph("=" * self.COLUMN_WIDTH) + + def linebreak(self): + return "\n" + + def list(self, body, ordered=True): + def _get_list_marker(): + decimal = count(1) + return lambda _: "{}.".format(next(decimal)) if ordered else "â€ĸ" + + return "".join( + ( + self.linebreak(), + re.sub( + magic_sequence_regex, + _get_list_marker(), + body, + ), + ) + ) + + def list_item(self, text): + return "".join( + ( + self.linebreak(), + MAGIC_SEQUENCE, + " ", + text.strip(), + ) + ) + + def paragraph(self, text): + if text.strip(): + return "".join( + ( + self.linebreak() * 2, + text, + ) + ) + return "" + + def block_quote(self, text): + return text + + def link(self, link, title, content): + return "".join( + ( + content, + " ({})".format(title) if title else "", + ": ", + link, + ) + ) + + def autolink(self, link, is_email=False): # noqa + return link + + +class NotifyEmailPreheaderMarkdownRenderer(NotifyPlainTextEmailMarkdownRenderer): + def header(self, text, level, raw=None): # noqa + return self.paragraph(text) + + def hrule(self): + return "" + + def link(self, link, title, content): + return "".join( + ( + content, + " ({})".format(title) if title else "", + ) + ) + + +notify_email_markdown = mistune.Markdown( + renderer=NotifyEmailMarkdownRenderer(), + hard_wrap=True, + use_xhtml=False, +) +notify_plain_text_email_markdown = mistune.Markdown( + renderer=NotifyPlainTextEmailMarkdownRenderer(), + hard_wrap=True, +) +notify_email_preheader_markdown = mistune.Markdown( + renderer=NotifyEmailPreheaderMarkdownRenderer(), + hard_wrap=True, +) +notify_letter_preview_markdown = mistune.Markdown( + renderer=NotifyLetterMarkdownPreviewRenderer(), + hard_wrap=True, + use_xhtml=False, +) diff --git a/notifications_utils/postal_address.py b/notifications_utils/postal_address.py new file mode 100644 index 000000000..9e0a008bd --- /dev/null +++ b/notifications_utils/postal_address.py @@ -0,0 +1,185 @@ +import re +from functools import lru_cache + +from notifications_utils.countries import UK, Country, CountryNotFoundError +from notifications_utils.countries.data import Postage +from notifications_utils.formatters import ( + get_lines_with_normalised_whitespace, + remove_whitespace, + remove_whitespace_before_punctuation, +) + +address_lines_1_to_6_keys = [ + # The API only accepts snake_case placeholders + "address_line_1", + "address_line_2", + "address_line_3", + "address_line_4", + "address_line_5", + "address_line_6", +] +address_lines_1_to_6_and_postcode_keys = address_lines_1_to_6_keys + ["postcode"] +address_line_7_key = "address_line_7" +address_lines_1_to_7_keys = address_lines_1_to_6_keys + [address_line_7_key] +country_UK = Country(UK) + + +class PostalAddress: + MIN_LINES = 3 + MAX_LINES = 7 + INVALID_CHARACTERS_AT_START_OF_ADDRESS_LINE = r'[\/()@]<>",=~' + + def __init__(self, raw_address, allow_international_letters=False): + self.raw_address = raw_address + self.allow_international_letters = allow_international_letters + + self._lines = [ + remove_whitespace_before_punctuation(line.rstrip(" ,")) + for line in get_lines_with_normalised_whitespace(self.raw_address) + if line.rstrip(" ,") + ] or [""] + + try: + self.country = Country(self._lines[-1]) + self._lines_without_country = self._lines[:-1] + except CountryNotFoundError: + self._lines_without_country = self._lines + self.country = country_UK + + def __bool__(self): + return bool(self.normalised) + + def __repr__(self): + return f"{self.__class__.__name__}({repr(self.raw_address)})" + + @classmethod + def from_personalisation( + cls, personalisation_dict, allow_international_letters=False + ): + if address_line_7_key in personalisation_dict: + keys = address_lines_1_to_6_keys + [address_line_7_key] + else: + keys = address_lines_1_to_6_and_postcode_keys + return cls( + "\n".join(str(personalisation_dict.get(key) or "") for key in keys), + allow_international_letters=allow_international_letters, + ) + + @property + def as_personalisation(self): + lines = dict.fromkeys(address_lines_1_to_6_keys, "") + lines.update( + { + f"address_line_{index}": value + for index, value in enumerate(self.normalised_lines[:-1], start=1) + if index < 7 + } + ) + lines["postcode"] = lines["address_line_7"] = self.normalised_lines[-1] + return lines + + @property + def as_single_line(self): + return ", ".join(self.normalised_lines) + + @property + def line_count(self): + return len(self.normalised.splitlines()) + + @property + def has_enough_lines(self): + return self.line_count >= self.MIN_LINES + + @property + def has_too_many_lines(self): + return self.line_count > self.MAX_LINES + + @property + def has_valid_postcode(self): + return self.postcode is not None + + @property + def has_valid_last_line(self): + return ( + self.allow_international_letters and self.international + ) or self.has_valid_postcode + + @property + def has_invalid_characters(self): + return any( + line.startswith(tuple(self.INVALID_CHARACTERS_AT_START_OF_ADDRESS_LINE)) + for line in self.normalised_lines + ) + + @property + def international(self): + return self.postage != Postage.UK + + @property + def normalised(self): + return "\n".join(self.normalised_lines) + + @property + def normalised_lines(self): + if self.international: + return self._lines_without_country + [self.country.canonical_name] + + if self.postcode: + return self._lines_without_country[:-1] + [self.postcode] + + return self._lines_without_country + + @property + def postage(self): + return self.country.postage_zone + + @property + def postcode(self): + if self.international: + return None + return format_postcode_or_none(self._lines_without_country[-1]) + + @property + def valid(self): + return ( + self.has_valid_last_line + and self.has_enough_lines + and not self.has_too_many_lines + and not self.has_invalid_characters + ) + + +def normalise_postcode(postcode): + return remove_whitespace(postcode).upper() + + +def is_a_real_uk_postcode(postcode): + standard = r"([A-Z]{1,2}[0-9][0-9A-Z]?[0-9][A-BD-HJLNP-UW-Z]{2})" + bfpo = r"(BFPO?(C\/O)?[0-9]{1,4})" + girobank = r"(GIR0AA)" + pattern = r"{}|{}|{}".format(standard, bfpo, girobank) + + return bool(re.fullmatch(pattern, normalise_postcode(postcode))) + + +def format_postcode_for_printing(postcode): + """ + This function formats the postcode so that it is ready for automatic sorting by Royal Mail. + :param String postcode: A postcode that's already been validated by is_a_real_uk_postcode + """ + postcode = normalise_postcode(postcode) + if "BFPOC/O" in postcode: + return postcode[:4] + " C/O " + postcode[7:] + elif "BFPO" in postcode: + return postcode[:4] + " " + postcode[4:] + return postcode[:-3] + " " + postcode[-3:] + + +# When processing an address we look at the postcode twice when +# normalising it, and once when validating it. So 8 is chosen because +# it’s 3, doubled to give some headroom then rounded up to the nearest +# power of 2 +@lru_cache(maxsize=8) +def format_postcode_or_none(postcode): + if is_a_real_uk_postcode(postcode): + return format_postcode_for_printing(postcode) diff --git a/notifications_utils/recipients.py b/notifications_utils/recipients.py new file mode 100644 index 000000000..0d8536c33 --- /dev/null +++ b/notifications_utils/recipients.py @@ -0,0 +1,743 @@ +import csv +import re +import sys +from collections import namedtuple +from contextlib import suppress +from functools import lru_cache +from io import StringIO +from itertools import islice + +import phonenumbers +from flask import current_app +from ordered_set import OrderedSet +from phonenumbers.phonenumberutil import NumberParseException + +from notifications_utils.formatters import ( + strip_all_whitespace, + strip_and_remove_obscure_whitespace, +) +from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.international_billing_rates import ( + INTERNATIONAL_BILLING_RATES, +) +from notifications_utils.postal_address import ( + address_line_7_key, + address_lines_1_to_6_and_postcode_keys, + address_lines_1_to_7_keys, +) +from notifications_utils.template import Template + +from . import EMAIL_REGEX_PATTERN, hostname_part, tld_part + +us_prefix = "1" + +first_column_headings = { + "email": ["email address"], + "sms": ["phone number"], + "letter": [ + line.replace("_", " ") + for line in address_lines_1_to_6_and_postcode_keys + [address_line_7_key] + ], +} + +address_columns = InsensitiveDict.from_keys(first_column_headings["letter"]) + + +class RecipientCSV: + max_rows = 100_000 + + def __init__( + self, + file_data, + template, + max_errors_shown=20, + max_initial_rows_shown=10, + guestlist=None, + remaining_messages=sys.maxsize, + allow_international_sms=False, + allow_international_letters=False, + should_validate=True, + ): + self.file_data = strip_all_whitespace(file_data, extra_characters=",") + self.max_errors_shown = max_errors_shown + self.max_initial_rows_shown = max_initial_rows_shown + self.guestlist = guestlist + self.template = template + self.allow_international_sms = allow_international_sms + self.allow_international_letters = allow_international_letters + self.remaining_messages = remaining_messages + self.rows_as_list = None + self.should_validate = should_validate + + def __len__(self): + if not hasattr(self, "_len"): + self._len = len(self.rows) + return self._len + + def __getitem__(self, requested_index): + return self.rows[requested_index] + + @property + def guestlist(self): + return self._guestlist + + @guestlist.setter + def guestlist(self, value): + try: + self._guestlist = list(value) + except TypeError: + self._guestlist = [] + + @property + def template(self): + return self._template + + @template.setter + def template(self, value): + if not isinstance(value, Template): + raise TypeError( + "template must be an instance of " + "notifications_utils.template.Template" + ) + self._template = value + self.template_type = self._template.template_type + self.recipient_column_headers = first_column_headings[self.template_type] + self.placeholders = self._template.placeholders + + @property + def placeholders(self): + return self._placeholders + + @placeholders.setter + def placeholders(self, value): + try: + self._placeholders = list(value) + self.recipient_column_headers + except TypeError: + self._placeholders = self.recipient_column_headers + self.placeholders_as_column_keys = [ + InsensitiveDict.make_key(placeholder) for placeholder in self._placeholders + ] + self.recipient_column_headers_as_column_keys = [ + InsensitiveDict.make_key(placeholder) + for placeholder in self.recipient_column_headers + ] + + @property + def has_errors(self): + return bool( + self.missing_column_headers + or self.duplicate_recipient_column_headers + or self.more_rows_than_can_send + or self.too_many_rows + or (not self.allowed_to_send_to) + or any(self.rows_with_errors) + ) # `or` is 3x faster than using `any()` here + + @property + def allowed_to_send_to(self): + if self.template_type == "letter": + return True + if not self.guestlist: + return True + return all( + allowed_to_send_to(row.recipient, self.guestlist) for row in self.rows + ) + + @property + def rows(self): + if self.rows_as_list is None: + self.rows_as_list = list(self.get_rows()) + return self.rows_as_list + + @property + def _rows(self): + return csv.reader( + StringIO(self.file_data.strip()), + quoting=csv.QUOTE_MINIMAL, + skipinitialspace=True, + ) + + def get_rows(self): + column_headers = self._raw_column_headers # this is for caching + length_of_column_headers = len(column_headers) + + rows_as_lists_of_columns = self._rows + + next(rows_as_lists_of_columns, None) # skip the header row + + for index, row in enumerate(rows_as_lists_of_columns): + if index >= self.max_rows: + yield None + continue + + output_dict = {} + + for column_name, column_value in zip(column_headers, row): + column_value = strip_and_remove_obscure_whitespace(column_value) + + if ( + InsensitiveDict.make_key(column_name) + in self.recipient_column_headers_as_column_keys + ): + output_dict[column_name] = column_value or None + else: + insert_or_append_to_dict( + output_dict, column_name, column_value or None + ) + + length_of_row = len(row) + + if length_of_column_headers < length_of_row: + output_dict[None] = row[length_of_column_headers:] + elif length_of_column_headers > length_of_row: + for key in column_headers[length_of_row:]: + insert_or_append_to_dict(output_dict, key, None) + + yield Row( + output_dict, + index=index, + error_fn=self._get_error_for_field, + recipient_column_headers=self.recipient_column_headers, + placeholders=self.placeholders_as_column_keys, + template=self.template, + allow_international_letters=self.allow_international_letters, + validate_row=self.should_validate, + ) + + @property + def more_rows_than_can_send(self): + return len(self) > self.remaining_messages + + @property + def too_many_rows(self): + return len(self) > self.max_rows + + @property + def initial_rows(self): + return islice(self.rows, self.max_initial_rows_shown) + + @property + def displayed_rows(self): + if any(self.rows_with_errors) and not self.missing_column_headers: + return self.initial_rows_with_errors + return self.initial_rows + + def _filter_rows(self, attr): + return (row for row in self.rows if row and getattr(row, attr)) + + @property + def rows_with_errors(self): + return self._filter_rows("has_error") + + @property + def rows_with_bad_recipients(self): + return self._filter_rows("has_bad_recipient") + + @property + def rows_with_missing_data(self): + return self._filter_rows("has_missing_data") + + @property + def rows_with_message_too_long(self): + return self._filter_rows("message_too_long") + + @property + def rows_with_empty_message(self): + return self._filter_rows("message_empty") + + @property + def initial_rows_with_errors(self): + return islice(self.rows_with_errors, self.max_errors_shown) + + @property + def _raw_column_headers(self): + for row in self._rows: + return row + return [] + + @property + def column_headers(self): + return list(OrderedSet(self._raw_column_headers)) + + @property + def column_headers_as_column_keys(self): + return InsensitiveDict.from_keys(self.column_headers).keys() + + @property + def missing_column_headers(self): + return set( + key + for key in self.placeholders + if ( + InsensitiveDict.make_key(key) not in self.column_headers_as_column_keys + and not self.is_address_column(key) + ) + ) + + @property + def duplicate_recipient_column_headers(self): + raw_recipient_column_headers = [ + InsensitiveDict.make_key(column_header) + for column_header in self._raw_column_headers + if InsensitiveDict.make_key(column_header) + in self.recipient_column_headers_as_column_keys + ] + + return OrderedSet( + ( + column_header + for column_header in self._raw_column_headers + if raw_recipient_column_headers.count( + InsensitiveDict.make_key(column_header) + ) + > 1 + ) + ) + + def is_address_column(self, key): + return self.template_type == "letter" and key in address_columns + + @property + def count_of_required_recipient_columns(self): + return 3 if self.template_type == "letter" else 1 + + @property + def has_recipient_columns(self): + if self.template_type == "letter": + sets_to_check = [ + InsensitiveDict.from_keys( + address_lines_1_to_6_and_postcode_keys + ).keys(), + InsensitiveDict.from_keys(address_lines_1_to_7_keys).keys(), + ] + else: + sets_to_check = [ + self.recipient_column_headers_as_column_keys, + ] + + for set_to_check in sets_to_check: + if ( + len( + # Work out which columns are shared between the possible + # letter address columns and the columns in the user’s + # spreadsheet (`&` means set intersection) + set_to_check + & self.column_headers_as_column_keys + ) + >= self.count_of_required_recipient_columns + ): + return True + + return False + + def _get_error_for_field(self, key, value): # noqa: C901 + if self.is_address_column(key): + return + + if ( + InsensitiveDict.make_key(key) + in self.recipient_column_headers_as_column_keys + ): + if value in [None, ""] or isinstance(value, list): + if self.duplicate_recipient_column_headers: + return None + else: + return Cell.missing_field_error + + try: + if self.template_type == "email": + validate_email_address(value) + if self.template_type == "sms": + validate_phone_number( + value, international=self.allow_international_sms + ) + except (InvalidEmailError, InvalidPhoneError) as error: + return str(error) + + if InsensitiveDict.make_key(key) not in self.placeholders_as_column_keys: + return + + if value in [None, ""]: + return Cell.missing_field_error + + +class Row(InsensitiveDict): + message_too_long = False + message_empty = False + + def __init__( + self, + row_dict, + *, + index, + error_fn, + recipient_column_headers, + placeholders, + template, + allow_international_letters, + validate_row=True, + ): + # If we don't need to validate, then: + # by not setting template we avoid the template level validation (used to check message length) + # by not setting error_fn, we avoid the Cell.__init__ validation (used to check phone nums are valid, + # placeholders are present, etc) + if not validate_row: + template = None + error_fn = None + + self.index = index + self.recipient_column_headers = recipient_column_headers + self.placeholders = placeholders + self.allow_international_letters = allow_international_letters + + if template: + template.values = row_dict + self.template_type = template.template_type + # we do not validate email size for CSVs to avoid performance issues + if self.template_type == "email": + self.message_too_long = False + else: + self.message_too_long = template.is_message_too_long() + self.message_empty = template.is_message_empty() + + super().__init__( + { + key: Cell(key, value, error_fn, self.placeholders) + for key, value in row_dict.items() + } + ) + + def __getitem__(self, key): + return super().__getitem__(key) if key in self else Cell() + + def get(self, key, default=None): + if key not in self and default is not None: + return default + return self[key] + + @property + def has_error(self): + return self.has_error_spanning_multiple_cells or any( + cell.error for cell in self.values() + ) + + @property + def has_bad_recipient(self): + if self.template_type == "letter": + return self.has_bad_postal_address + return self.get(self.recipient_column_headers[0]).recipient_error + + @property + def has_bad_postal_address(self): + return self.template_type == "letter" and not self.as_postal_address.valid + + @property + def has_error_spanning_multiple_cells(self): + return ( + self.message_too_long or self.message_empty or self.has_bad_postal_address + ) + + @property + def has_missing_data(self): + return any(cell.error == Cell.missing_field_error for cell in self.values()) + + @property + def recipient(self): + columns = [self.get(column).data for column in self.recipient_column_headers] + return columns[0] if len(columns) == 1 else columns + + @property + def as_postal_address(self): + from notifications_utils.postal_address import PostalAddress + + return PostalAddress.from_personalisation( + self.recipient_and_personalisation, + allow_international_letters=self.allow_international_letters, + ) + + @property + def personalisation(self): + return InsensitiveDict( + {key: cell.data for key, cell in self.items() if key in self.placeholders} + ) + + @property + def recipient_and_personalisation(self): + return InsensitiveDict({key: cell.data for key, cell in self.items()}) + + +class Cell: + missing_field_error = "Missing" + + def __init__(self, key=None, value=None, error_fn=None, placeholders=None): + self.data = value + self.error = error_fn(key, value) if error_fn else None + self.ignore = InsensitiveDict.make_key(key) not in (placeholders or []) + + def __eq__(self, other): + if not other.__class__ == self.__class__: + return False + return all( + ( + self.data == other.data, + self.error == other.error, + self.ignore == other.ignore, + ) + ) + + @property + def recipient_error(self): + return self.error not in {None, self.missing_field_error} + + +class InvalidEmailError(Exception): + def __init__(self, message=None): + super().__init__(message or "Not a valid email address") + + +class InvalidPhoneError(InvalidEmailError): + pass + + +class InvalidAddressError(InvalidEmailError): + pass + + +def normalize_phone_number(phonenumber): + if isinstance(phonenumber, str): + phonenumber = phonenumbers.parse(phonenumber, "US") + return phonenumbers.format_number(phonenumber, phonenumbers.PhoneNumberFormat.E164) + + +def is_us_phone_number(number): + try: + return _get_country_code(number) == us_prefix + except NumberParseException: + return False + + +international_phone_info = namedtuple( + "PhoneNumber", + [ + "international", + "country_prefix", + "billable_units", + ], +) + + +def get_international_phone_info(number): + number = validate_phone_number(number, international=True) + prefix = _get_country_code(number) + + return international_phone_info( + international=(prefix != us_prefix), + country_prefix=prefix, + billable_units=get_billable_units_for_prefix(prefix), + ) + + +# NANP_COUNTRY_AREA_CODES are the list of area codes in the North American Numbering Plan +# that have their own entry in international_billing_rates.yml. +# Source: https://en.wikipedia.org/wiki/List_of_North_American_Numbering_Plan_area_codes +_NANP_COUNTRY_AREA_CODES = [ + "684", + "242", + "246", + "264", + "268", + "284", + "345", + "441", + "473", + "649", + "876", + "664", + "721", + "758", + "767", + "784", + "868", + "869", +] + + +def _get_country_code(number): + parsed = phonenumbers.parse(number, "US") + country_code = str(parsed.country_code) + if country_code == us_prefix: + area_code = str(parsed.national_number)[:3] + if area_code in _NANP_COUNTRY_AREA_CODES: + return f"{country_code}{area_code}" + return country_code + + +def get_billable_units_for_prefix(prefix): + """Return the billable units for prefix. Hard-coded to 1 for now""" + return 1 + # return INTERNATIONAL_BILLING_RATES[prefix]['billable_units'] + + +def use_numeric_sender(number): + prefix = _get_country_code(number) + return ( + INTERNATIONAL_BILLING_RATES[(prefix or us_prefix)]["attributes"]["alpha"] + == "NO" + ) + + +def validate_us_phone_number(number): + try: + parsed = phonenumbers.parse(number, "US") + if not is_us_phone_number(number): + raise InvalidPhoneError("Not a US number") + if phonenumbers.is_valid_number(parsed): + return normalize_phone_number(parsed) + if len(str(parsed.national_number)) > 10: + raise InvalidPhoneError("Too many digits") + if len(str(parsed.national_number)) < 10: + raise InvalidPhoneError("Not enough digits") + if phonenumbers.is_possible_number(parsed): + raise InvalidPhoneError("Phone number range is not in use") + raise InvalidPhoneError("Phone number is not possible") + except NumberParseException as exc: + raise InvalidPhoneError(exc._msg) from exc + + +def validate_phone_number(number, international=False): + if (not international) or is_us_phone_number(number): + return validate_us_phone_number(number) + + try: + parsed = phonenumbers.parse(number, None) + if parsed.country_code != 1: + raise InvalidPhoneError("Invalid country code") + number = f"{parsed.country_code}{parsed.national_number}" + if len(number) < 8: + raise InvalidPhoneError("Not enough digits") + if len(number) > 15: + raise InvalidPhoneError("Too many digits") + return normalize_phone_number(parsed) + except NumberParseException as exc: + if exc._msg == "Could not interpret numbers after plus-sign.": + raise InvalidPhoneError("Not a valid country prefix") from exc + raise InvalidPhoneError(exc._msg) from exc + + +validate_and_format_phone_number = validate_phone_number + + +def try_validate_and_format_phone_number(number, international=None, log_msg=None): + """ + For use in places where you shouldn't error if the phone number is invalid - for example if firetext pass us + something in + """ + try: + return validate_and_format_phone_number(number, international) + except InvalidPhoneError as exc: + if log_msg: + current_app.logger.warning("{}: {}".format(log_msg, exc)) + return number + + +def _do_simple_email_checks(match, email_address): + # not an email + if not match: + raise InvalidEmailError + + if len(email_address) > 320: + raise InvalidEmailError + + # don't allow consecutive periods in either part + if ".." in email_address: + raise InvalidEmailError + + +def validate_email_address(email_address): # noqa (C901 too complex) + # almost exactly the same as by https://github.com/wtforms/wtforms/blob/master/wtforms/validators.py, + # with minor tweaks for SES compatibility - to avoid complications we are a lot stricter with the local part + # than neccessary - we don't allow any double quotes or semicolons to prevent SES Technical Failures + email_address = strip_and_remove_obscure_whitespace(email_address) + match = re.match(EMAIL_REGEX_PATTERN, email_address) + + _do_simple_email_checks(match, email_address) + + hostname = match.group(1) + + # idna = "Internationalized domain name" - this encode/decode cycle converts unicode into its accurate ascii + # representation as the web uses. '䞋え.テ゚ト'.encode('idna') == b'xn--r8jz45g.xn--zckzah' + try: + hostname = hostname.encode("idna").decode("ascii") + except UnicodeError: + raise InvalidEmailError + + parts = hostname.split(".") + + if len(hostname) > 253 or len(parts) < 2: + raise InvalidEmailError + + for part in parts: + if not part or len(part) > 63 or not hostname_part.match(part): + raise InvalidEmailError + + # if the part after the last . is not a valid TLD then bail out + if not tld_part.match(parts[-1]): + raise InvalidEmailError + + return email_address + + +def format_email_address(email_address): + return strip_and_remove_obscure_whitespace(email_address.lower()) + + +def validate_and_format_email_address(email_address): + return format_email_address(validate_email_address(email_address)) + + +@lru_cache(maxsize=32, typed=False) +def format_recipient(recipient): + if not isinstance(recipient, str): + return "" + with suppress(InvalidPhoneError): + return validate_and_format_phone_number(recipient, international=True) + with suppress(InvalidEmailError): + return validate_and_format_email_address(recipient) + return recipient + + +def format_phone_number_human_readable(phone_number): + try: + phone_number = validate_phone_number(phone_number, international=True) + except InvalidPhoneError: + # if there was a validation error, we want to shortcut out here, but still display the number on the front end + return phone_number + international_phone_info = get_international_phone_info(phone_number) + + return phonenumbers.format_number( + phonenumbers.parse(phone_number, None), + ( + phonenumbers.PhoneNumberFormat.INTERNATIONAL + if international_phone_info.international + else phonenumbers.PhoneNumberFormat.NATIONAL + ), + ) + + +def allowed_to_send_to(recipient, allowlist): + return format_recipient(recipient) in {format_recipient(x) for x in allowlist} + + +def insert_or_append_to_dict(dict_, key, value): + if not (key or value): + # We don’t care about completely empty values so it’s faster to + # ignore them rather than working out how to store them + return + + if dict_.get(key): + if isinstance(dict_[key], list): + dict_[key].append(value) + else: + dict_[key] = [dict_[key], value] + else: + dict_.update({key: value}) diff --git a/notifications_utils/request_helper.py b/notifications_utils/request_helper.py new file mode 100644 index 000000000..6c9edc8a7 --- /dev/null +++ b/notifications_utils/request_helper.py @@ -0,0 +1,123 @@ +from flask import abort, current_app, request +from flask.wrappers import Request + +TRACE_ID_HEADER = "X-B3-TraceId" +SPAN_ID_HEADER = "X-B3-SpanId" +PARENT_SPAN_ID_HEADER = "X-B3-ParentSpanId" + + +class NotifyRequest(Request): + """ + A custom Request class, implementing extraction of zipkin headers used to trace request through cloudfoundry + as described here: https://docs.cloudfoundry.org/concepts/http-routing.html#zipkin-headers + """ + + @property + def request_id(self): + return self.trace_id + + @property + def trace_id(self): + """ + The "trace id" (in zipkin terms) assigned to this request, if present (None otherwise) + """ + if not hasattr(self, "_trace_id"): + self._trace_id = self._get_header_value(TRACE_ID_HEADER) + return self._trace_id + + @property + def span_id(self): + """ + The "span id" (in zipkin terms) set in this request's header, if present (None otherwise) + """ + if not hasattr(self, "_span_id"): + # note how we don't generate an id of our own. not being supplied a span id implies that we are running in + # an environment with no span-id-aware request router, and thus would have no intermediary to prevent the + # propagation of our span id all the way through all our onwards requests much like trace id. and the point + # of span id is to assign identifiers to each individual request. + self._span_id = self._get_header_value(SPAN_ID_HEADER) + return self._span_id + + @property + def parent_span_id(self): + """ + The "parent span id" (in zipkin terms) set in this request's header, if present (None otherwise) + """ + if not hasattr(self, "_parent_span_id"): + self._parent_span_id = self._get_header_value(PARENT_SPAN_ID_HEADER) + return self._parent_span_id + + def _get_header_value(self, header_name): + """ + Returns value of the given header + """ + if header_name in self.headers and self.headers[header_name]: + return self.headers[header_name] + + return None + + +class ResponseHeaderMiddleware(object): + def __init__(self, app): + self._app = app + + def __call__(self, environ, start_response): + req = NotifyRequest(environ) + + def rewrite_response_headers(status, headers, exc_info=None): + lower_existing_header_names = frozenset( + name.lower() for name, value in headers + ) + + if TRACE_ID_HEADER.lower() not in lower_existing_header_names: + headers.append((TRACE_ID_HEADER, str(req.trace_id))) + + if SPAN_ID_HEADER.lower() not in lower_existing_header_names: + headers.append((SPAN_ID_HEADER, str(req.span_id))) + + return start_response(status, headers, exc_info) + + return self._app(environ, rewrite_response_headers) + + +def init_app(app): + app.request_class = NotifyRequest + app.wsgi_app = ResponseHeaderMiddleware(app.wsgi_app) + + +def check_proxy_header_before_request(): + keys = [ + current_app.config.get("ROUTE_SECRET_KEY_1"), + current_app.config.get("ROUTE_SECRET_KEY_2"), + ] + result, msg = _check_proxy_header_secret(request, keys) + + if not result: + if current_app.config.get("CHECK_PROXY_HEADER", False): + current_app.logger.warning(msg) + abort(403) + + # We need to return None to continue processing the request + # http://flask.pocoo.org/docs/0.12/api/#flask.Flask.before_request + return None + + +def _check_proxy_header_secret(request, secrets, header="X-Custom-Forwarder"): + if header not in request.headers: + return False, "Header missing" + + header_secret = request.headers.get(header) + if not header_secret: + return False, "Header exists but is empty" + + # if there isn't any non-empty secret configured we fail closed + if not any(secrets): + return False, "Secrets are not configured" + + for i, secret in enumerate(secrets): + if header_secret == secret: + return True, "Key used: {}".format( + i + 1 + ) # add 1 to make it human-compatible + + return False, "Header didn't match any keys" diff --git a/notifications_utils/s3.py b/notifications_utils/s3.py new file mode 100644 index 000000000..cdcc70a5c --- /dev/null +++ b/notifications_utils/s3.py @@ -0,0 +1,87 @@ +import os +import urllib + +import botocore +from boto3 import Session +from botocore.config import Config +from flask import current_app + +AWS_CLIENT_CONFIG = Config( + # This config is required to enable S3 to connect to FIPS-enabled + # endpoints. See https://aws.amazon.com/compliance/fips/ for more + # information. + s3={ + "addressing_style": "virtual", + }, + use_fips_endpoint=True, +) + +default_access_key_id = os.environ.get("AWS_ACCESS_KEY_ID") +default_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY") +default_region = os.environ.get("AWS_REGION") + + +def s3upload( + filedata, + region, + bucket_name, + file_location, + content_type="binary/octet-stream", + tags=None, + metadata=None, + access_key=default_access_key_id, + secret_key=default_secret_access_key, +): + session = Session( + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + ) + _s3 = session.resource("s3", config=AWS_CLIENT_CONFIG) + + key = _s3.Object(bucket_name, file_location) + + put_args = { + "Body": filedata, + "ServerSideEncryption": "AES256", + "ContentType": content_type, + } + + if tags: + tags = urllib.parse.urlencode(tags) + put_args["Tagging"] = tags + + if metadata: + metadata = put_args["Metadata"] = metadata + + try: + key.put(**put_args) + except botocore.exceptions.ClientError as e: + current_app.logger.error( + "Unable to upload file to S3 bucket {}".format(bucket_name) + ) + raise e + + +class S3ObjectNotFound(botocore.exceptions.ClientError): + pass + + +def s3download( + bucket_name, + filename, + region=default_region, + access_key=default_access_key_id, + secret_key=default_secret_access_key, +): + try: + session = Session( + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + region_name=region, + ) + s3 = session.resource("s3", config=AWS_CLIENT_CONFIG) + key = s3.Object(bucket_name, filename) + return key.get()["Body"] + except botocore.exceptions.ClientError as error: + raise S3ObjectNotFound(error.response, error.operation_name) diff --git a/notifications_utils/safe_string.py b/notifications_utils/safe_string.py new file mode 100644 index 000000000..be49c81a4 --- /dev/null +++ b/notifications_utils/safe_string.py @@ -0,0 +1,25 @@ +import re +import unicodedata + + +def make_string_safe(string, whitespace): + # strips accents, diacritics etc + string = "".join( + c + for c in unicodedata.normalize("NFD", string) + if unicodedata.category(c) != "Mn" + ) + string = "".join( + word.lower() if word.isalnum() or word == whitespace else "" + for word in re.sub(r"\s+", whitespace, string.strip()) + ) + string = re.sub(r"\.{2,}", ".", string) + return string.strip(".") + + +def make_string_safe_for_email_local_part(string): + return make_string_safe(string, whitespace=".") + + +def make_string_safe_for_id(string): + return make_string_safe(string, whitespace="-") diff --git a/notifications_utils/sanitise_text.py b/notifications_utils/sanitise_text.py new file mode 100644 index 000000000..3e9da0764 --- /dev/null +++ b/notifications_utils/sanitise_text.py @@ -0,0 +1,310 @@ +import ast +import unicodedata + +from regex import regex + + +class SanitiseText: + ALLOWED_CHARACTERS = set() + + REPLACEMENT_CHARACTERS = { + "–": "-", # EN DASH (U+2013) + "—": "-", # EM DASH (U+2014) + "â€Ļ": "...", # HORIZONTAL ELLIPSIS (U+2026) + "‘": "'", # LEFT SINGLE QUOTATION MARK (U+2018) + "’": "'", # RIGHT SINGLE QUOTATION MARK (U+2019) + "“": '"', # LEFT DOUBLE QUOTATION MARK (U+201C) + "”": '"', # RIGHT DOUBLE QUOTATION MARK (U+201D) + "\u180E": "", # Mongolian vowel separator + "\u200B": "", # zero width space + "\u200C": "", # zero width non-joiner + "\u200D": "", # zero width joiner + "\u2060": "", # word joiner + "\uFEFF": "", # zero width non-breaking space + "\u00A0": " ", # NON BREAKING WHITE SPACE (U+200B) + "\t": " ", # TAB + } + + @classmethod + def encode(cls, content): + return "".join(cls.encode_char(char) for char in content) + + @classmethod + def get_non_compatible_characters(cls, content): + """ + Given an input string, return a set of non compatible characters. + + This follows the same rules as `cls.encode`, but returns just the characters that encode would replace with `?` + """ + return set( + c + for c in content + if c not in cls.ALLOWED_CHARACTERS + and cls.is_extended_language(c) is False + and cls.downgrade_character(c) is None + ) + + @staticmethod + def get_unicode_char_from_codepoint(codepoint): + """ + Given a unicode codepoint (eg 002E for '.', 0061 for 'a', etc), return that actual unicode character. + + unicodedata.decomposition returns strings containing codepoints, so we need to eval them ourselves + """ + # lets just make sure we aren't evaling anything weird + if not set(codepoint) <= set("0123456789ABCDEF") or not len(codepoint) == 4: + raise ValueError("{} is not a valid unicode codepoint".format(codepoint)) + return ast.literal_eval('"\\u{}"'.format(codepoint)) + + @classmethod + def downgrade_character(cls, c): + """ + Attempt to downgrade a non-compatible character to the allowed character set. May downgrade to multiple + characters, eg `â€Ļ -> ...` + + Will return None if character is either already valid or has no known downgrade + """ + decomposed = unicodedata.decomposition(c) + if decomposed != "" and "<" not in decomposed: + # decomposition lists the unicode code points a character is made up of, if it's made up of multiple + # points. For example the ÃĄ character returns '0061 0301', as in, the character a, followed by a combining + # acute accent. The decomposition might, however, also contain a decomposition mapping in angle brackets. + # For a full list of the types, see here: https://www.compart.com/en/unicode/decomposition. + # If it's got a mapping, we're not sure how best to downgrade it, so just see if it's in the + # REPLACEMENT_CHARACTERS map. If not, then it's probably a letter with a modifier, eg ÃĄ + # ASSUMPTION: The first character of a combined unicode character (eg 'ÃĄ' == '0061 0301') + # will be the ascii char + return cls.get_unicode_char_from_codepoint(decomposed.split()[0]) + else: + # try and find a mapping (eg en dash -> hyphen ('–': '-')), else return None + return cls.REPLACEMENT_CHARACTERS.get(c) + + @classmethod + def is_japanese(cls, value): + if regex.search(r"([\p{IsHan}\p{IsHiragana}\p{IsKatakana}]+)", value): + return True + return False + + @classmethod + def is_chinese(cls, value): + # This range supports all "CJK Unified Ideoglyphs" + # It may be missing some rare/historic characters that are not in common use + if regex.search(r"[\u4e00-\u9fff]+", value) or value in [ + "。", + "、", + "īŧš", + "īŧŸ", + "īŧ", + ";", + "(", + ")", + "“", + "”", + "īŧŒ", + ]: + return True + return False + + @classmethod + def is_arabic(cls, value): + # For some reason, the python definition of Arabic (IsArabic) doesn't include + # some standard diacritics, so add them here. + if ( + regex.search(r"\p{IsArabic}", value) + or regex.search(r"[\uFE70]+", value) + or regex.search(r"[\u064B]+", value) + or regex.search(r"[\u064F]+", value) + ): + return True + return False + + @classmethod + def is_punjabi(cls, value): + # Gukmukhi script or Shahmukhi script + + if regex.search(r"[\u0A00-\u0A7F]+", value): + return True + elif regex.search(r"[\u0600-\u06FF]+", value): + return True + elif regex.search(r"[\u0750-\u077F]+", value): + return True + elif regex.search(r"[\u08A0-\u08FF]+", value): + return True + elif regex.search(r"[\uFB50-\uFDFF]+", value): + return True + elif regex.search(r"[\uFE70-\uFEFF]+", value): + return True + elif regex.search(r"[\u0900-\u097F]+", value): + return True + return False + + @classmethod + def _is_extended_language_group_one(cls, value): + if regex.search(r"\p{IsHangul}", value): # Korean + return True + elif regex.search(r"\p{IsCyrillic}", value): + return True + elif SanitiseText.is_arabic(value): + return True + elif regex.search(r"\p{IsArmenian}", value): + return True + elif regex.search(r"\p{IsBengali}", value): + return True + elif SanitiseText.is_punjabi(value): + return True + return False + + @classmethod + def _is_extended_language_group_two(cls, value): + if regex.search(r"\p{IsBuhid}", value): + return True + if regex.search(r"\p{IsCanadian_Aboriginal}", value): + return True + if regex.search(r"\p{IsCherokee}", value): + return True + if regex.search(r"\p{IsDevanagari}", value): + return True + if regex.search(r"\p{IsEthiopic}", value): + return True + if regex.search(r"\p{IsGeorgian}", value): + return True + return False + + @classmethod + def _is_extended_language_group_three(cls, value): + if regex.search(r"\p{IsGreek}", value): + return True + if regex.search(r"\p{IsGujarati}", value): + return True + if regex.search(r"\p{IsHanunoo}", value): + return True + if regex.search(r"\p{IsHebrew}", value): + return True + if regex.search(r"\p{IsLimbu}", value): + return True + if regex.search(r"\p{IsKannada}", value): + return True + return False + + @classmethod + def _is_extended_language_group_four(cls, value): + if regex.search( + r"([\p{IsKhmer}\p{IsLao}\p{IsMongolian}\p{IsMyanmar}\p{IsTibetan}\p{IsYi}]+)", + value, + ): + return True + + if regex.search( + r"([\p{IsOgham}\p{IsOriya}\p{IsSinhala}\p{IsSyriac}\p{IsTagalog}]+)", value + ): + return True + + if regex.search( + r"([\p{IsTagbanwa}\p{IsTaiLe}\p{IsTamil}\p{IsTelugu}\p{IsThaana}\p{IsThai}]+)", + value, + ): + return True + + # Vietnamese + if regex.search( + r"\b\S*[AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´AĂÂÁáēŽáē¤Ã€áē°áēĻáēĸáē˛áē¨Ãƒáē´áēĒáē áēļáēŦĐEÊÉáēžÃˆáģ€áēēáģ‚áēŧáģ„áē¸áģ†IÍÌáģˆÄ¨áģŠOÔƠÓáģáģšÃ’áģ’áģœáģŽáģ”áģžÃ•áģ–áģ áģŒáģ˜áģĸUƯÚáģ¨Ã™áģĒáģĻáģŦŨáģŽáģ¤áģ°YÝáģ˛áģļáģ¸áģ´Ä‘a-zA-Z]+\S*\b", # noqa + value, + ): + return True + + # Turkish + if regex.search(r"\b\S*[a-zA-ZÃ§ÄŸÄąÅŸÃļÃŧÇĞİŞÖÜ]+\S*\b", value): + return True + + return False + + @classmethod + def is_extended_language(cls, value): + """ + Languages are combined in groups to handle cyclomatic complexity warnings + """ + if cls._is_extended_language_group_one(value): + return True + if cls._is_extended_language_group_two(value): + return True + if cls._is_extended_language_group_three(value): + return True + if cls.is_japanese(value): + return True + if cls._is_extended_language_group_four(value): + return True + if cls.is_chinese(value): + return True + + return False + + @classmethod + def encode_char(cls, c): + """ + Given a single unicode character, return a compatible character from the allowed set. + """ + # char is a good character already - return that native character. + if c in cls.ALLOWED_CHARACTERS: + return c + elif cls.is_extended_language(c): + return c + else: + c = cls.downgrade_character(c) + return c if c is not None else "?" + + +class SanitiseSMS(SanitiseText): + """ + Given an input string, makes it GSM and Welsh character compatible. This involves removing all non-gsm characters by + applying the following rules + * characters within the GSM character set (https://en.wikipedia.org/wiki/GSM_03.38) + and extension character set are kept + + * Welsh characters not included in the default GSM character set are kept + + * characters with sensible downgrades are replaced in place + * characters with diacritics (accents, umlauts, cedillas etc) are replaced with their base character, eg Ê -> e + * en dash and em dash (– and —) are replaced with hyphen (-) + * left/right quotation marks (‘, ’, “, ”) are replaced with ' and " + * zero width spaces (sometimes used to stop eg "gov.uk" linkifying) are removed + * tabs are replaced with a single space + + * any remaining unicode characters (eg chinese/cyrillic/glyphs/emoji) are replaced with ? + """ + + WELSH_DIACRITICS = set( + "àèÃŦÃ˛Ãšáēáģŗ" + "ÀÈÌÒÙáē€áģ˛" # grave + "ÃĄÃŠÃ­ÃŗÃēáēƒÃŊ" + "ÁÉÍÓÚáē‚Ý" # acute + "äÃĢïÃļÃŧáē…Ãŋ" + "ÄËÏÖÜáē„Ÿ" # diaeresis + "ÃĸÃĒÎôÃģÅĩŎ" + "ÂÊÎÔÛŴÅļ" # carets + ) + + EXTENDED_GSM_CHARACTERS = set("^{}\\[~]|â‚Ŧ") + + GSM_CHARACTERS = ( + set( + "@ÂŖ$ÂĨèÊÚÃŦÃ˛Ã‡\nØø\rÅÃĨΔ_ÎĻÎ“Î›ÎŠÎ Î¨ÎŖÎ˜Îž\x1bÆÃĻßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" + + "ÂĄABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧÂŋabcdefghijklmnopqrstuvwxyzäÃļÃąÃŧà" + ) + | EXTENDED_GSM_CHARACTERS + ) + + ALLOWED_CHARACTERS = GSM_CHARACTERS | WELSH_DIACRITICS + # some welsh characters are in GSM and some aren't - we need to distinguish between these for counting fragments + WELSH_NON_GSM_CHARACTERS = WELSH_DIACRITICS - GSM_CHARACTERS + + +class SanitiseASCII(SanitiseText): + """ + As SMS above, but the allowed characters are printable ascii, from character range 32 to 126 inclusive. + [chr(x) for x in range(32, 127)] + """ + + ALLOWED_CHARACTERS = set( + " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + ) diff --git a/notifications_utils/serialised_model.py b/notifications_utils/serialised_model.py new file mode 100644 index 000000000..8e925008e --- /dev/null +++ b/notifications_utils/serialised_model.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod + + +class SerialisedModel(ABC): + """ + A SerialisedModel takes a dictionary, typically created by + serialising a database object. It then takes the value of specified + keys from the dictionary and adds them to itself as properties, so + that it can be interacted with like any other object. It is cleaner + and safer than dealing with dictionaries directly because it + guarantees that: + - all of the ALLOWED_PROPERTIES are present in the underlying + dictionary + - any other abritrary properties of the underlying dictionary can’t + be accessed + + If you are adding a new field to a model, you should ensure that + all sources of the cache data are updated to return that new field, + then clear the cache, before adding that field to the + ALLOWED_PROPERTIES list. + """ + + @property + @abstractmethod + def ALLOWED_PROPERTIES(self): + pass + + def __init__(self, _dict): + for property in self.ALLOWED_PROPERTIES: + setattr(self, property, _dict[property]) + + +class SerialisedModelCollection(ABC): + """ + A SerialisedModelCollection takes a list of dictionaries, typically + created by serialising database objects. When iterated over it + returns a model instance for each of the items in the list. + """ + + @property + @abstractmethod + def model(self): + pass + + def __init__(self, items): + self.items = items + + def __bool__(self): + return bool(self.items) + + def __getitem__(self, index): + return self.model(self.items[index]) + + def __len__(self): + return len(self.items) + + def __add__(self, other): + return list(self) + list(other) + + def __radd__(self, other): + return list(other) + list(self) diff --git a/notifications_utils/take.py b/notifications_utils/take.py new file mode 100644 index 000000000..ea46c39cf --- /dev/null +++ b/notifications_utils/take.py @@ -0,0 +1,3 @@ +class Take(str): + def then(self, func, *args, **kwargs): + return self.__class__(func(self, *args, **kwargs)) diff --git a/notifications_utils/template.py b/notifications_utils/template.py new file mode 100644 index 000000000..302fd3899 --- /dev/null +++ b/notifications_utils/template.py @@ -0,0 +1,977 @@ +import math +import re +from abc import ABC, abstractmethod +from datetime import datetime +from functools import lru_cache +from html import unescape +from os import path + +from jinja2 import Environment, FileSystemLoader, select_autoescape +from markupsafe import Markup + +from notifications_utils import ( + LETTER_MAX_PAGE_COUNT, + MAGIC_SEQUENCE, + SMS_CHAR_COUNT_LIMIT, +) +from notifications_utils.countries.data import Postage +from notifications_utils.field import Field, PlainTextField +from notifications_utils.formatters import ( + add_prefix, + add_trailing_newline, + autolink_urls, + escape_html, + formatted_list, + make_quotes_smart, + nl2br, + normalise_multiple_newlines, + normalise_whitespace, + normalise_whitespace_and_newlines, + remove_smart_quotes_from_email_addresses, + remove_whitespace_before_punctuation, + replace_hyphens_with_en_dashes, + replace_hyphens_with_non_breaking_hyphens, + sms_encode, + strip_leading_whitespace, + strip_unsupported_characters, + unlink_govuk_escaped, +) +from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.markdown import ( + notify_email_markdown, + notify_email_preheader_markdown, + notify_letter_preview_markdown, + notify_plain_text_email_markdown, +) +from notifications_utils.postal_address import ( + PostalAddress, + address_lines_1_to_7_keys, +) +from notifications_utils.sanitise_text import SanitiseSMS +from notifications_utils.take import Take +from notifications_utils.template_change import TemplateChange + +template_env = Environment( + autoescape=select_autoescape(), + loader=FileSystemLoader( + path.join( + path.dirname(path.abspath(__file__)), + "jinja_templates", + ) + ), +) + + +class Template(ABC): + encoding = "utf-8" + + def __init__( + self, + template, + values=None, + redact_missing_personalisation=False, + ): + if not isinstance(template, dict): + raise TypeError("Template must be a dict") + if values is not None and not isinstance(values, dict): + raise TypeError("Values must be a dict") + if template.get("template_type") != self.template_type: + raise TypeError( + f"Cannot initialise {self.__class__.__name__} " + f'with {template.get("template_type")} template_type' + ) + self.id = template.get("id", None) + self.name = template.get("name", None) + self.content = template["content"] + self.values = values + self._template = template + self.redact_missing_personalisation = redact_missing_personalisation + + def __repr__(self): + return '{}("{}", {})'.format(self.__class__.__name__, self.content, self.values) + + @abstractmethod + def __str__(self): + pass + + @property + def content_with_placeholders_filled_in(self): + return str( + Field( + self.content, + self.values, + html="passthrough", + redact_missing_personalisation=self.redact_missing_personalisation, + markdown_lists=True, + ) + ).strip() + + @property + def values(self): + if hasattr(self, "_values"): + return self._values + return {} + + @values.setter + def values(self, value): + if not value: + self._values = {} + else: + placeholders = InsensitiveDict.from_keys(self.placeholders) + self._values = InsensitiveDict(value).as_dict_with_keys( + self.placeholders + | set( + key + for key in value.keys() + if InsensitiveDict.make_key(key) not in placeholders.keys() + ) + ) + + @property + def placeholders(self): + return get_placeholders(self.content) + + @property + def missing_data(self): + return list( + placeholder + for placeholder in self.placeholders + if self.values.get(placeholder) is None + ) + + @property + def additional_data(self): + return self.values.keys() - self.placeholders + + def get_raw(self, key, default=None): + return self._template.get(key, default) + + def compare_to(self, new): + return TemplateChange(self, new) + + @property + def content_count(self): + return len(self.content_with_placeholders_filled_in) + + def is_message_empty(self): + if not self.content: + return True + + if not self.content.startswith("((") or not self.content.endswith("))"): + # If the content doesn’t start or end with a placeholder we + # can guarantee it’s not empty, no matter what + # personalisation has been provided. + return False + + return self.content_count == 0 + + def is_message_too_long(self): + return False + + +class BaseSMSTemplate(Template): + template_type = "sms" + + def __init__( + self, + template, + values=None, + prefix=None, + show_prefix=True, + sender=None, + ): + self.prefix = prefix + self.show_prefix = show_prefix + self.sender = sender + self._content_count = None + super().__init__(template, values) + + @property + def values(self): + return super().values + + @values.setter + def values(self, value): + # If we change the values of the template it’s possible the + # content count will have changed, so we need to reset the + # cached count. + if self._content_count is not None: + self._content_count = None + + # Assigning to super().values doesn’t work here. We need to get + # the property object instead, which has the special method + # fset, which invokes the setter it as if we were + # assigning to it outside this class. + super(BaseSMSTemplate, type(self)).values.fset(self, value) + + @property + def content_with_placeholders_filled_in(self): + # We always call SMSMessageTemplate.__str__ regardless of + # subclass, to avoid any HTML formatting. SMS templates differ + # in that the content can include the service name as a prefix. + # So historically we’ve returned the fully-formatted message, + # rather than some plain-text represenation of the content. To + # preserve compatibility for consumers of the API we maintain + # that behaviour by overriding this method here. + return SMSMessageTemplate.__str__(self) + + @property + def prefix(self): + return self._prefix if self.show_prefix else None + + @prefix.setter + def prefix(self, value): + self._prefix = value + + @property + def content_count(self): + """ + Return the number of characters in the message. Note that we don't distinguish between GSM and non-GSM + characters at this point, as `get_sms_fragment_count` handles that separately. + + Also note that if values aren't provided, will calculate the raw length of the unsubstituted placeholders, + as in the message `foo ((placeholder))` has a length of 19. + """ + if self._content_count is None: + self._content_count = len(self._get_unsanitised_content()) + return self._content_count + + @property + def content_count_without_prefix(self): + # subtract 2 extra characters to account for the colon and the space, + # added max zero in case the content is empty the __str__ methods strips the white space. + if self.prefix: + return max((self.content_count - len(self.prefix) - 2), 0) + else: + return self.content_count + + @property + def fragment_count(self): + """ + A fragment is up to 140 bytes, which could consist of 160 GSM chars, 140 ascii chars, or 70 ucs-2 chars, + or any combination thereof. + + Since we are supporting more or less "all" languages, it doesn't seem like we really want to count chars, + and that counting bytes should suffice. + """ + + # check if all chars are in the GSM-7 character set + def gsm_check(x): + rule = re.compile( + r'^[\sa-zA-Z0-9_@?ÂŖ!1$"ÂĨ#è?¤Ê%Ú&ÃŦ\\Ã˛(Ç)*:Ø+;ÄäøÆ,ÜÃŧÃĨÉ/Â§Ã ÂĄÂŋ\']+$' + ) + gsm_match = rule.search(x) + if gsm_match is None: + return False + return True + + message_str = self.content_with_placeholders_filled_in + + content_len = len(message_str) + + """ + Checks for GSM-7 char set, calculates msg size, and + then fragments based on multipart message rules. ASCII + was not specifically called out as almost all messages will + switch from 7bit GSM to Unicode. + + Calculations are based on https://messente.com/documentation/tools/sms-length-calculator + """ + if gsm_check(message_str): + if content_len <= 160: + return math.ceil(content_len / 160) + else: + return math.ceil(content_len / 153) + else: + if content_len <= 70: + return math.ceil(content_len / 70) + else: + return math.ceil(content_len / 67) + + def is_message_too_long(self): + """ + Message is validated with out the prefix. + We have decided to be lenient and let the message go over the character limit. The SMS provider will + send messages well over our limit. There were some inconsistencies with how we were validating the + length of a message. This should be the method used anytime we want to reject a message for being too long. + """ + return self.content_count_without_prefix > SMS_CHAR_COUNT_LIMIT + + def is_message_empty(self): + return self.content_count_without_prefix == 0 + + def _get_unsanitised_content(self): + # This is faster to call than SMSMessageTemplate.__str__ if all + # you need to know is how many characters are in the message + if self.values: + values = self.values + else: + values = {key: MAGIC_SEQUENCE for key in self.placeholders} + return ( + Take(PlainTextField(self.content, values, html="passthrough")) + .then(add_prefix, self.prefix) + .then(remove_whitespace_before_punctuation) + .then(normalise_whitespace_and_newlines) + .then(normalise_multiple_newlines) + .then(str.strip) + .then(str.replace, MAGIC_SEQUENCE, "") + ) + + +class SMSMessageTemplate(BaseSMSTemplate): + def __str__(self): + return sms_encode(self._get_unsanitised_content()) + + +class SMSBodyPreviewTemplate(BaseSMSTemplate): + def __init__( + self, + template, + values=None, + ): + super().__init__(template, values, show_prefix=False) + + def __str__(self): + return Markup( + Take( + Field( + self.content, + self.values, + html="escape", + redact_missing_personalisation=True, + ) + ) + .then(sms_encode) + .then(remove_whitespace_before_punctuation) + .then(normalise_whitespace_and_newlines) + .then(normalise_multiple_newlines) + .then(str.strip) + ) + + +class SMSPreviewTemplate(BaseSMSTemplate): + jinja_template = template_env.get_template("sms_preview_template.jinja2") + + def __init__( + self, + template, + values=None, + prefix=None, + show_prefix=True, + sender=None, + show_recipient=False, + show_sender=False, + downgrade_non_sms_characters=True, + redact_missing_personalisation=False, + ): + self.show_recipient = show_recipient + self.show_sender = show_sender + self.downgrade_non_sms_characters = downgrade_non_sms_characters + super().__init__(template, values, prefix, show_prefix, sender) + self.redact_missing_personalisation = redact_missing_personalisation + + def __str__(self): + return Markup( + self.jinja_template.render( + { + "sender": self.sender, + "show_sender": self.show_sender, + "recipient": Field( + "((phone number))", + self.values, + with_brackets=False, + html="escape", + ), + "show_recipient": self.show_recipient, + "body": Take( + Field( + self.content, + self.values, + html="escape", + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then( + add_prefix, + ( + (escape_html(self.prefix) or None) + if self.show_prefix + else None + ), + ) + .then(sms_encode if self.downgrade_non_sms_characters else str) + .then(remove_whitespace_before_punctuation) + .then(normalise_whitespace_and_newlines) + .then(normalise_multiple_newlines) + .then(nl2br) + .then( + autolink_urls, + classes="govuk-link govuk-link--no-visited-state", + ), + } + ) + ) + + +class BaseBroadcastTemplate(BaseSMSTemplate): + template_type = "broadcast" + + MAX_CONTENT_COUNT_GSM = 1_395 + MAX_CONTENT_COUNT_UCS2 = 615 + + @property + def encoded_content_count(self): + if self.non_gsm_characters: + return self.content_count + return self.content_count + count_extended_gsm_chars( + self.content_with_placeholders_filled_in + ) + + @property + def non_gsm_characters(self): + return non_gsm_characters(self.content) + + @property + def max_content_count(self): + if self.non_gsm_characters: + return self.MAX_CONTENT_COUNT_UCS2 + return self.MAX_CONTENT_COUNT_GSM + + @property + def content_too_long(self): + return self.encoded_content_count > self.max_content_count + + +class BroadcastPreviewTemplate(BaseBroadcastTemplate, SMSPreviewTemplate): + jinja_template = template_env.get_template("broadcast_preview_template.jinja2") + + +class BroadcastMessageTemplate(BaseBroadcastTemplate, SMSMessageTemplate): + @classmethod + def from_content(cls, content): + return cls( + template={ + "template_type": cls.template_type, + "content": content, + }, + values=None, # events have already done interpolation of any personalisation + ) + + @classmethod + def from_event(cls, broadcast_event): + """ + should be directly callable with the results of the BroadcastEvent.serialize() function from api/models.py + """ + return cls.from_content(broadcast_event["transmitted_content"]["body"]) + + def __str__(self): + return ( + Take( + Field( + self.content.strip(), + self.values, + html="escape", + ) + ) + .then(sms_encode) + .then(remove_whitespace_before_punctuation) + .then(normalise_whitespace_and_newlines) + .then(normalise_multiple_newlines) + ) + + +class SubjectMixin: + def __init__(self, template, values=None, **kwargs): + self._subject = template["subject"] + super().__init__(template, values, **kwargs) + + @property + def subject(self): + return Markup( + Take( + Field( + self._subject, + self.values, + html="escape", + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then(do_nice_typography) + .then(normalise_whitespace) + ) + + @property + def placeholders(self): + return get_placeholders(self._subject) | super().placeholders + + +class BaseEmailTemplate(SubjectMixin, Template): + template_type = "email" + + @property + def html_body(self): + return ( + Take( + Field( + self.content, + self.values, + html="escape", + markdown_lists=True, + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then(unlink_govuk_escaped) + .then(strip_unsupported_characters) + .then(add_trailing_newline) + .then(notify_email_markdown) + .then(do_nice_typography) + ) + + @property + def content_size_in_bytes(self): + return len(self.content_with_placeholders_filled_in.encode("utf8")) + + def is_message_too_long(self): + """ + SES rejects email messages bigger than 10485760 bytes (just over 10 MB per message (after base64 encoding)): + https://docs.aws.amazon.com/ses/latest/DeveloperGuide/quotas.html#limits-message + + Base64 is apparently wasteful because we use just 64 different values per byte, whereas a byte can represent + 256 different characters. That is, we use bytes (which are 8-bit words) as 6-bit words. There is + a waste of 2 bits for each 8 bits of transmission data. To send three bytes of information + (3 times 8 is 24 bits), you need to use four bytes (4 times 6 is again 24 bits). Thus the base64 version + of a file is 4/3 larger than it might be. So we use 33% more storage than we could. + https://lemire.me/blog/2019/01/30/what-is-the-space-overhead-of-base64-encoding/ + + That brings down our max safe size to 7.5 MB == 7500000 bytes before base64 encoding + + But this is not the end! The message we send to SES is structured as follows: + "Message": { + 'Subject': { + 'Data': subject, + }, + 'Body': {'Text': {'Data': body}, 'Html': {'Data': html_body}} + }, + Which means that we are sending the contents of email message twice in one request: once in plain text + and once with html tags. That means our plain text content needs to be much shorter to make sure we + fit within the limit, especially since HTML body can be much byte-heavier than plain text body. + + Hence, we decided to put the limit at 1MB, which is equivalent of between 250 and 500 pages of text. + That's still an extremely long email, and should be sufficient for all normal use, while at the same + time giving us safe margin while sending the emails through Amazon SES. + + EDIT: putting size up to 2MB as GOV.UK email digests are hitting the limit. + """ + return self.content_size_in_bytes > 2000000 + + +class PlainTextEmailTemplate(BaseEmailTemplate): + def __str__(self): + return ( + Take( + Field( + self.content, self.values, html="passthrough", markdown_lists=True + ) + ) + .then(unlink_govuk_escaped) + .then(strip_unsupported_characters) + .then(add_trailing_newline) + .then(notify_plain_text_email_markdown) + .then(do_nice_typography) + .then(unescape) + .then(strip_leading_whitespace) + .then(add_trailing_newline) + ) + + @property + def subject(self): + return Markup( + Take( + Field( + self._subject, + self.values, + html="passthrough", + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then(do_nice_typography) + .then(normalise_whitespace) + ) + + +class HTMLEmailTemplate(BaseEmailTemplate): + jinja_template = template_env.get_template("email_template.jinja2") + + PREHEADER_LENGTH_IN_CHARACTERS = 256 + + def __init__( + self, + template, + values=None, + govuk_banner=True, + complete_html=True, + brand_logo=None, + brand_text=None, + brand_colour=None, + brand_banner=False, + brand_name=None, + ): + super().__init__(template, values) + self.govuk_banner = govuk_banner + self.complete_html = complete_html + self.brand_logo = brand_logo + self.brand_text = brand_text + self.brand_colour = brand_colour + self.brand_banner = brand_banner + self.brand_name = brand_name + + @property + def preheader(self): + return " ".join( + Take( + Field( + self.content, + self.values, + html="escape", + markdown_lists=True, + ) + ) + .then(unlink_govuk_escaped) + .then(strip_unsupported_characters) + .then(add_trailing_newline) + .then(notify_email_preheader_markdown) + .then(do_nice_typography) + .split() + )[: self.PREHEADER_LENGTH_IN_CHARACTERS].strip() + + def __str__(self): + return self.jinja_template.render( + { + "subject": self.subject, + "body": self.html_body, + "preheader": self.preheader, + "govuk_banner": self.govuk_banner, + "complete_html": self.complete_html, + "brand_logo": self.brand_logo, + "brand_text": self.brand_text, + "brand_colour": self.brand_colour, + "brand_banner": self.brand_banner, + "brand_name": self.brand_name, + } + ) + + +class EmailPreviewTemplate(BaseEmailTemplate): + jinja_template = template_env.get_template("email_preview_template.jinja2") + + def __init__( + self, + template, + values=None, + from_name=None, + from_address=None, + reply_to=None, + show_recipient=True, + redact_missing_personalisation=False, + ): + super().__init__( + template, + values, + redact_missing_personalisation=redact_missing_personalisation, + ) + self.from_name = from_name + self.from_address = from_address + self.reply_to = reply_to + self.show_recipient = show_recipient + + def __str__(self): + return Markup( + self.jinja_template.render( + { + "body": self.html_body, + "subject": self.subject, + "from_name": escape_html(self.from_name), + "from_address": self.from_address, + "reply_to": self.reply_to, + "recipient": Field( + "((email address))", self.values, with_brackets=False + ), + "show_recipient": self.show_recipient, + } + ) + ) + + @property + def subject(self): + return ( + Take( + Field( + self._subject, + self.values, + html="escape", + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then(do_nice_typography) + .then(normalise_whitespace) + ) + + +class BaseLetterTemplate(SubjectMixin, Template): + template_type = "letter" + + address_block = "\n".join( + f'(({line.replace("_", " ")}))' for line in address_lines_1_to_7_keys + ) + + def __init__( + self, + template, + values=None, + contact_block=None, + admin_base_url="http://localhost:6012", + logo_file_name=None, + redact_missing_personalisation=False, + date=None, + ): + self.contact_block = (contact_block or "").strip() + super().__init__( + template, + values, + redact_missing_personalisation=redact_missing_personalisation, + ) + self.admin_base_url = admin_base_url + self.logo_file_name = logo_file_name + self.date = date or datetime.utcnow() + + @property + def subject(self): + return ( + Take( + Field( + self._subject, + self.values, + redact_missing_personalisation=self.redact_missing_personalisation, + html="escape", + ) + ) + .then(do_nice_typography) + .then(normalise_whitespace) + ) + + @property + def placeholders(self): + return get_placeholders(self.contact_block) | super().placeholders + + @property + def postal_address(self): + return PostalAddress.from_personalisation(InsensitiveDict(self.values)) + + @property + def _address_block(self): + if ( + self.postal_address.has_enough_lines + and not self.postal_address.has_too_many_lines + ): + return self.postal_address.normalised_lines + + if "address line 7" not in self.values and "postcode" in self.values: + self.values["address line 7"] = self.values["postcode"] + + return Field( + self.address_block, + self.values, + html="escape", + with_brackets=False, + ).splitlines() + + @property + def _contact_block(self): + return ( + Take( + Field( + "\n".join(line.strip() for line in self.contact_block.split("\n")), + self.values, + redact_missing_personalisation=self.redact_missing_personalisation, + html="escape", + ) + ) + .then(remove_whitespace_before_punctuation) + .then(nl2br) + ) + + @property + def _date(self): + return self.date.strftime("%-d %B %Y") + + @property + def _message(self): + return ( + Take( + Field( + self.content, + self.values, + html="escape", + markdown_lists=True, + redact_missing_personalisation=self.redact_missing_personalisation, + ) + ) + .then(add_trailing_newline) + .then(notify_letter_preview_markdown) + .then(do_nice_typography) + .then(replace_hyphens_with_non_breaking_hyphens) + ) + + +class LetterPreviewTemplate(BaseLetterTemplate): + jinja_template = template_env.get_template("letter_pdf/preview.jinja2") + + def __str__(self): + return Markup( + self.jinja_template.render( + { + "admin_base_url": self.admin_base_url, + "logo_file_name": self.logo_file_name, + # logo_class should only ever be None, svg or png + "logo_class": ( + self.logo_file_name.lower()[-3:] + if self.logo_file_name + else None + ), + "subject": self.subject, + "message": self._message, + "address": self._address_block, + "contact_block": self._contact_block, + "date": self._date, + } + ) + ) + + +class LetterPrintTemplate(LetterPreviewTemplate): + jinja_template = template_env.get_template("letter_pdf/print.jinja2") + + +class LetterImageTemplate(BaseLetterTemplate): + jinja_template = template_env.get_template("letter_image_template.jinja2") + first_page_number = 1 + allowed_postage_types = ( + Postage.FIRST, + Postage.SECOND, + Postage.EUROPE, + Postage.REST_OF_WORLD, + ) + + def __init__( + self, + template, + values=None, + image_url=None, + page_count=None, + contact_block=None, + postage=None, + ): + super().__init__(template, values, contact_block=contact_block) + if not image_url: + raise TypeError("image_url is required") + if not page_count: + raise TypeError("page_count is required") + if postage not in [None] + list(self.allowed_postage_types): + raise TypeError( + "postage must be None, {}".format( + formatted_list( + self.allowed_postage_types, + conjunction="or", + before_each="'", + after_each="'", + ) + ) + ) + self.image_url = image_url + self.page_count = int(page_count) + self._postage = postage + + @property + def postage(self): + if self.postal_address.international: + return self.postal_address.postage + return self._postage + + @property + def last_page_number(self): + return min(self.page_count, LETTER_MAX_PAGE_COUNT) + self.first_page_number + + @property + def page_numbers(self): + return list(range(self.first_page_number, self.last_page_number)) + + @property + def postage_description(self): + return { + Postage.FIRST: "first class", + Postage.SECOND: "second class", + Postage.EUROPE: "international", + Postage.REST_OF_WORLD: "international", + }.get(self.postage) + + @property + def postage_class_value(self): + return { + Postage.FIRST: "letter-postage-first", + Postage.SECOND: "letter-postage-second", + Postage.EUROPE: "letter-postage-international", + Postage.REST_OF_WORLD: "letter-postage-international", + }.get(self.postage) + + def __str__(self): + return Markup( + self.jinja_template.render( + { + "image_url": self.image_url, + "page_numbers": self.page_numbers, + "address": self._address_block, + "contact_block": self._contact_block, + "date": self._date, + "subject": self.subject, + "message": self._message, + "show_postage": bool(self.postage), + "postage_description": self.postage_description, + "postage_class_value": self.postage_class_value, + } + ) + ) + + +def get_sms_fragment_count(character_count, non_gsm_characters): + if non_gsm_characters: + return 1 if character_count <= 70 else math.ceil(float(character_count) / 67) + else: + return 1 if character_count <= 160 else math.ceil(float(character_count) / 153) + + +def non_gsm_characters(content): + """ + Returns a set of all the non gsm characters in a text. this doesn't include characters that we will downgrade (eg + emoji, ellipsis, Ãą, etc). This only includes welsh non gsm characters that will force the entire SMS to be encoded + with UCS-2. + """ + return set(content) & set(SanitiseSMS.WELSH_NON_GSM_CHARACTERS) + + +def count_extended_gsm_chars(content): + return sum(map(content.count, SanitiseSMS.EXTENDED_GSM_CHARACTERS)) + + +def do_nice_typography(value): + return ( + Take(value) + .then(remove_whitespace_before_punctuation) + .then(make_quotes_smart) + .then(remove_smart_quotes_from_email_addresses) + .then(replace_hyphens_with_en_dashes) + ) + + +@lru_cache(maxsize=1024) +def get_placeholders(content): + return Field(content).placeholders diff --git a/notifications_utils/template_change.py b/notifications_utils/template_change.py new file mode 100644 index 000000000..e47a271f6 --- /dev/null +++ b/notifications_utils/template_change.py @@ -0,0 +1,31 @@ +from ordered_set import OrderedSet + +from notifications_utils.insensitive_dict import InsensitiveDict + + +class TemplateChange: + def __init__(self, old_template, new_template): + self.old_placeholders = InsensitiveDict.from_keys(old_template.placeholders) + self.new_placeholders = InsensitiveDict.from_keys(new_template.placeholders) + + @property + def has_different_placeholders(self): + return bool(self.new_placeholders.keys() ^ self.old_placeholders.keys()) + + @property + def placeholders_added(self): + return OrderedSet( + [ + self.new_placeholders.get(key) + for key in self.new_placeholders.keys() - self.old_placeholders.keys() + ] + ) + + @property + def placeholders_removed(self): + return OrderedSet( + [ + self.old_placeholders.get(key) + for key in self.old_placeholders.keys() - self.new_placeholders.keys() + ] + ) diff --git a/notifications_utils/timezones.py b/notifications_utils/timezones.py new file mode 100644 index 000000000..277a139f0 --- /dev/null +++ b/notifications_utils/timezones.py @@ -0,0 +1,16 @@ +import os + +import pytz +from dateutil import parser + +local_timezone = pytz.timezone(os.getenv("TIMEZONE", "America/New_York")) + + +def utc_string_to_aware_gmt_datetime(date): + """ + Date can either be a string, naïve UTC datetime or an aware UTC datetime + Returns an aware local datetime, essentially the time you'd see on your clock + """ + date = parser.parse(date) + forced_utc = date.replace(tzinfo=pytz.utc) + return forced_utc.astimezone(local_timezone) diff --git a/notifications_utils/url_safe_token.py b/notifications_utils/url_safe_token.py new file mode 100644 index 000000000..502534809 --- /dev/null +++ b/notifications_utils/url_safe_token.py @@ -0,0 +1,13 @@ +from itsdangerous import URLSafeTimedSerializer + +from notifications_utils.formatters import url_encode_full_stops + + +def generate_token(payload, secret, salt): + return url_encode_full_stops(URLSafeTimedSerializer(secret).dumps(payload, salt)) + + +def check_token(token, secret, salt, max_age_seconds): + ser = URLSafeTimedSerializer(secret) + payload = ser.loads(token, max_age=max_age_seconds, salt=salt) + return payload diff --git a/poetry.lock b/poetry.lock index 5354ae9b6..4601694ab 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" @@ -2653,62 +2653,6 @@ docopt = ">=0.3.0" PyJWT = ">=1.5.1" requests = ">=2.0.0" -[[package]] -name = "notifications-utils" -version = "0.5.1" -description = "" -optional = false -python-versions = "^3.12.2" -files = [] -develop = false - -[package.dependencies] -async-timeout = "^4.0.2" -bleach = "^6.1.0" -blinker = "^1.7.0" -boto3 = "^1.34.83" -botocore = "^1.34.92" -cachetools = "^5.3.0" -certifi = "^2024.2.2" -cffi = "^1.16.0" -charset-normalizer = "^3.1.0" -click = "^8.1.7" -cryptography = "^42.0.4" -flask = "^3.0.3" -flask-redis = "^0.4.0" -geojson = "^3.0.1" -govuk-bank-holidays = "^0.14" -idna = "^3.7" -itsdangerous = "^2.1.2" -jinja2 = "^3.1.3" -jmespath = "^1.0.1" -markupsafe = "^2.1.5" -mistune = "==0.8.4" -numpy = "^1.24.2" -ordered-set = "^4.1.0" -phonenumbers = "^8.13.35" -pycparser = "^2.21" -python-dateutil = "^2.8.2" -python-json-logger = "^2.0.7" -pytz = "^2024.1" -pyyaml = "^6.0" -redis = "^5.0.3" -regex = "^2023.12.25" -requests = "^2.31.0" -s3transfer = "^0.10.1" -shapely = "^2.0.4" -six = "^1.16.0" -smartypants = "^2.0.1" -urllib3 = "^2.2.1" -webencodings = "^0.5.1" -werkzeug = "^3.0.1" - -[package.source] -type = "git" -url = "https://github.com/GSA/notifications-utils.git" -reference = "HEAD" -resolved_reference = "e047ba3e37f9fd885baafe6944a4910285a506fb" - [[package]] name = "numpy" version = "1.26.4" @@ -3511,7 +3455,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3698,104 +3641,90 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2023.12.25" +version = "2024.5.15" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"}, - {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"}, - {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"}, - {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"}, - {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"}, - {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"}, - {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"}, - {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"}, - {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"}, - {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"}, - {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"}, - {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"}, - {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"}, - {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"}, - {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"}, - {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"}, - {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"}, - {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"}, - {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"}, - {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"}, - {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"}, - {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"}, - {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"}, - {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"}, - {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"}, - {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"}, - {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"}, - {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"}, - {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"}, - {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"}, - {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"}, - {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"}, + {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"}, + {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"}, + {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"}, + {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"}, + {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"}, + {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"}, + {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"}, + {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"}, + {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"}, + {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"}, + {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"}, + {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"}, + {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"}, + {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"}, + {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"}, + {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"}, + {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"}, + {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"}, + {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"}, + {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"}, + {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"}, + {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"}, + {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"}, + {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"}, + {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"}, + {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"}, ] [[package]] @@ -4800,4 +4729,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12.2" -content-hash = "ed8a34b31a8739d1cb852c90f2004efccc00458c06fc5760a078b4225386e4db" +content-hash = "f471a058bdf49d0645c24438f5b0ce4b3038000c9185cbfeb5058ccb58c5724b" diff --git a/pyproject.toml b/pyproject.toml index e64316780..19b2e4a83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ marshmallow = "==3.21.2" marshmallow-sqlalchemy = "==1.0.0" newrelic = "*" notifications-python-client = "==9.0.0" -notifications-utils = {git = "https://github.com/GSA/notifications-utils.git"} oscrypto = "==1.3.0" packaging = "==24.0" poetry-dotenv-plugin = "==0.2.0" @@ -50,6 +49,19 @@ python-dotenv = "==1.0.1" sqlalchemy = "==2.0.30" werkzeug = "^3.0.3" faker = "^25.2.0" +async-timeout = "^4.0.3" +bleach = "^6.1.0" +geojson = "^3.1.0" +govuk-bank-holidays = "^0.14" +numpy = "^1.26.4" +ordered-set = "^4.1.0" +phonenumbers = "^8.13.36" +python-json-logger = "^2.0.7" +pytz = "^2024.1" +regex = "^2024.5.15" +shapely = "^2.0.4" +smartypants = "^2.0.1" +mistune = "0.8.4" [tool.poetry.group.dev.dependencies] diff --git a/tests/app/celery/test_scheduled_tasks.py b/tests/app/celery/test_scheduled_tasks.py index 1652700f0..73b6b6074 100644 --- a/tests/app/celery/test_scheduled_tasks.py +++ b/tests/app/celery/test_scheduled_tasks.py @@ -4,7 +4,6 @@ from unittest import mock from unittest.mock import ANY, call import pytest -from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket from app.celery import scheduled_tasks from app.celery.scheduled_tasks import ( @@ -19,6 +18,7 @@ from app.celery.scheduled_tasks import ( from app.config import QueueNames, Test from app.dao.jobs_dao import dao_get_job_by_id from app.enums import JobStatus, NotificationStatus, TemplateType +from notifications_utils.clients.zendesk.zendesk_client import NotifySupportTicket from tests.app import load_example_csv from tests.app.db import create_job, create_notification, create_template diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 7b1463d2c..8c5b264de 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -7,8 +7,6 @@ import pytest import requests_mock from celery.exceptions import Retry from freezegun import freeze_time -from notifications_utils.recipients import Row -from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate from requests import RequestException from sqlalchemy.exc import SQLAlchemyError @@ -39,6 +37,8 @@ from app.enums import ( from app.models import Job, Notification from app.serialised_models import SerialisedService, SerialisedTemplate from app.utils import DATETIME_FORMAT +from notifications_utils.recipients import Row +from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate from tests.app import load_example_csv from tests.app.db import ( create_api_key, diff --git a/tests/app/notifications/test_process_notification.py b/tests/app/notifications/test_process_notification.py index 160c96f97..8c80ad7a7 100644 --- a/tests/app/notifications/test_process_notification.py +++ b/tests/app/notifications/test_process_notification.py @@ -5,10 +5,6 @@ from collections import namedtuple import pytest from boto3.exceptions import Boto3Error from freezegun import freeze_time -from notifications_utils.recipients import ( - validate_and_format_email_address, - validate_and_format_phone_number, -) from sqlalchemy.exc import SQLAlchemyError from app.enums import KeyType, NotificationType, ServicePermissionType, TemplateType @@ -21,6 +17,10 @@ from app.notifications.process_notifications import ( ) from app.serialised_models import SerialisedTemplate from app.v2.errors import BadRequestError +from notifications_utils.recipients import ( + validate_and_format_email_address, + validate_and_format_phone_number, +) from tests.app.db import create_service, create_template diff --git a/tests/app/notifications/test_validators.py b/tests/app/notifications/test_validators.py index 42d96c93d..c6c317744 100644 --- a/tests/app/notifications/test_validators.py +++ b/tests/app/notifications/test_validators.py @@ -1,7 +1,6 @@ import pytest from flask import current_app from freezegun import freeze_time -from notifications_utils import SMS_CHAR_COUNT_LIMIT import app from app.dao import templates_dao @@ -37,6 +36,7 @@ from app.serialised_models import ( from app.service.utils import service_allowed_to_send_to from app.utils import get_template_instance from app.v2.errors import BadRequestError, RateLimitError, TotalRequestsError +from notifications_utils import SMS_CHAR_COUNT_LIMIT from tests.app.db import ( create_api_key, create_reply_to_email, diff --git a/tests/app/organization/test_invite_rest.py b/tests/app/organization/test_invite_rest.py index 0783b0c62..71e8c12ad 100644 --- a/tests/app/organization/test_invite_rest.py +++ b/tests/app/organization/test_invite_rest.py @@ -4,10 +4,10 @@ import uuid import pytest from flask import current_app, json from freezegun import freeze_time -from notifications_utils.url_safe_token import generate_token from app.enums import InvitedUserStatus from app.models import Notification +from notifications_utils.url_safe_token import generate_token from tests import create_admin_authorization_header from tests.app.db import create_invited_org_user diff --git a/tests/app/service/send_notification/test_send_notification.py b/tests/app/service/send_notification/test_send_notification.py index b1bd27988..1997c2bf3 100644 --- a/tests/app/service/send_notification/test_send_notification.py +++ b/tests/app/service/send_notification/test_send_notification.py @@ -5,7 +5,6 @@ import pytest from flask import current_app, json from freezegun import freeze_time from notifications_python_client.authentication import create_jwt_token -from notifications_utils import SMS_CHAR_COUNT_LIMIT import app from app.dao import notifications_dao @@ -17,6 +16,7 @@ from app.errors import InvalidRequest from app.models import ApiKey, Notification, NotificationHistory, Template from app.service.send_notification import send_one_off_notification from app.v2.errors import RateLimitError +from notifications_utils import SMS_CHAR_COUNT_LIMIT from tests import create_service_authorization_header from tests.app.db import ( create_api_key, diff --git a/tests/app/service/send_notification/test_send_one_off_notification.py b/tests/app/service/send_notification/test_send_one_off_notification.py index 000e22005..7c510fb8c 100644 --- a/tests/app/service/send_notification/test_send_one_off_notification.py +++ b/tests/app/service/send_notification/test_send_one_off_notification.py @@ -2,8 +2,6 @@ import uuid from unittest.mock import Mock import pytest -from notifications_utils import SMS_CHAR_COUNT_LIMIT -from notifications_utils.recipients import InvalidPhoneError from app.config import QueueNames from app.dao.service_guest_list_dao import dao_add_and_commit_guest_list_contacts @@ -18,6 +16,8 @@ from app.enums import ( from app.models import Notification, ServiceGuestList from app.service.send_notification import send_one_off_notification from app.v2.errors import BadRequestError +from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.recipients import InvalidPhoneError from tests.app.db import ( create_reply_to_email, create_service, diff --git a/tests/app/service_invite/test_service_invite_rest.py b/tests/app/service_invite/test_service_invite_rest.py index e736a3042..0f2f20b50 100644 --- a/tests/app/service_invite/test_service_invite_rest.py +++ b/tests/app/service_invite/test_service_invite_rest.py @@ -5,10 +5,10 @@ from functools import partial import pytest from flask import current_app from freezegun import freeze_time -from notifications_utils.url_safe_token import generate_token from app.enums import AuthType, InvitedUserStatus from app.models import Notification +from notifications_utils.url_safe_token import generate_token from tests import create_admin_authorization_header from tests.app.db import create_invited_user diff --git a/tests/app/template/test_rest.py b/tests/app/template/test_rest.py index 93b36939f..45dfc24f9 100644 --- a/tests/app/template/test_rest.py +++ b/tests/app/template/test_rest.py @@ -6,11 +6,11 @@ from datetime import datetime, timedelta import pytest from freezegun import freeze_time -from notifications_utils import SMS_CHAR_COUNT_LIMIT from app.dao.templates_dao import dao_get_template_by_id, dao_redact_template from app.enums import ServicePermissionType, TemplateProcessType, TemplateType from app.models import Template, TemplateHistory +from notifications_utils import SMS_CHAR_COUNT_LIMIT from tests import create_admin_authorization_header from tests.app.db import create_service, create_template, create_template_folder diff --git a/tests/notification_utils/__init__.py b/tests/notification_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/notification_utils/clients/antivirus/test_antivirus_client.py b/tests/notification_utils/clients/antivirus/test_antivirus_client.py new file mode 100644 index 000000000..a8f7a85bc --- /dev/null +++ b/tests/notification_utils/clients/antivirus/test_antivirus_client.py @@ -0,0 +1,63 @@ +import io + +import pytest +import requests + +from notifications_utils.clients.antivirus.antivirus_client import ( + AntivirusClient, + AntivirusError, +) + + +@pytest.fixture(scope="function") +def antivirus(app, mocker): + client = AntivirusClient() + app.config["ANTIVIRUS_API_HOST"] = "https://antivirus" + app.config["ANTIVIRUS_API_KEY"] = "test-antivirus-key" + client.init_app(app) + return client + + +def test_scan_document(antivirus, rmock): + document = io.BytesIO(b"filecontents") + rmock.request( + "POST", + "https://antivirus/scan", + json={"ok": True}, + request_headers={ + "Authorization": "Bearer test-antivirus-key", + }, + status_code=200, + ) + + resp = antivirus.scan(document) + + assert resp + assert "filecontents" in rmock.last_request.text + assert document.tell() == 0 + + +def test_should_raise_for_status(antivirus, rmock): + with pytest.raises(AntivirusError) as excinfo: + rmock.request( + "POST", + "https://antivirus/scan", + json={"error": "Antivirus error"}, + status_code=400, + ) + + antivirus.scan(io.BytesIO(b"document")) + + assert excinfo.value.message == "Antivirus error" + assert excinfo.value.status_code == 400 + + +def test_should_raise_for_connection_errors(antivirus, rmock): + with pytest.raises(AntivirusError) as excinfo: + rmock.request( + "POST", "https://antivirus/scan", exc=requests.exceptions.ConnectTimeout + ) + antivirus.scan(io.BytesIO(b"document")) + + assert excinfo.value.message == "connection error" + assert excinfo.value.status_code == 503 diff --git a/tests/notification_utils/clients/encryption/test_encryption_client.py b/tests/notification_utils/clients/encryption/test_encryption_client.py new file mode 100644 index 000000000..c392ba529 --- /dev/null +++ b/tests/notification_utils/clients/encryption/test_encryption_client.py @@ -0,0 +1,88 @@ +import pytest + +from notifications_utils.clients.encryption.encryption_client import ( + Encryption, + EncryptionError, +) + + +@pytest.fixture() +def encryption_client(app): + client = Encryption() + + app.config["SECRET_KEY"] = "test-notify-secret-key" + app.config["DANGEROUS_SALT"] = "test-notify-salt" + + client.init_app(app) + + return client + + +def test_should_ensure_shared_salt_security(app): + client = Encryption() + app.config["SECRET_KEY"] = "test-notify-secret-key" + app.config["DANGEROUS_SALT"] = "too-short" + with pytest.raises(EncryptionError): + client.init_app(app) + + +def test_should_ensure_custom_salt_security(encryption_client): + with pytest.raises(EncryptionError): + encryption_client.encrypt("this", salt="too-short") + + +def test_should_encrypt_strings(encryption_client): + encrypted = encryption_client.encrypt("this") + assert encrypted != "this" + assert isinstance(encrypted, str) + + +def test_should_encrypt_dicts(encryption_client): + to_encrypt = {"hello": "world"} + encrypted = encryption_client.encrypt(to_encrypt) + assert encrypted != to_encrypt + assert encryption_client.decrypt(encrypted) == to_encrypt + + +def test_encryption_is_nondeterministic(encryption_client): + first_run = encryption_client.encrypt("this") + second_run = encryption_client.encrypt("this") + assert first_run != second_run + + +def test_should_decrypt_content(encryption_client): + encrypted = encryption_client.encrypt("this") + assert encryption_client.decrypt(encrypted) == "this" + + +def test_should_decrypt_content_with_custom_salt(encryption_client): + salt = "different-salt-value" + encrypted = encryption_client.encrypt("this", salt=salt) + assert encryption_client.decrypt(encrypted, salt=salt) == "this" + + +def test_should_verify_decryption(encryption_client): + encrypted = encryption_client.encrypt("this") + with pytest.raises(EncryptionError): + encryption_client.decrypt(encrypted, salt="different-salt-value") + + +def test_should_sign_and_serialize_string(encryption_client): + signed = encryption_client.sign("this") + assert signed != "this" + + +def test_should_verify_signature_and_deserialize_string(encryption_client): + signed = encryption_client.sign("this") + assert encryption_client.verify_signature(signed) == "this" + + +def test_should_raise_encryption_error_on_bad_salt(encryption_client): + signed = encryption_client.sign("this") + with pytest.raises(EncryptionError): + encryption_client.verify_signature(signed, salt="different-salt-value") + + +def test_should_sign_and_serialize_json(encryption_client): + signed = encryption_client.sign({"this": "that"}) + assert encryption_client.verify_signature(signed) == {"this": "that"} diff --git a/tests/notification_utils/clients/redis/test_redis_client.py b/tests/notification_utils/clients/redis/test_redis_client.py new file mode 100644 index 000000000..1f122206b --- /dev/null +++ b/tests/notification_utils/clients/redis/test_redis_client.py @@ -0,0 +1,221 @@ +import uuid +from datetime import datetime +from unittest.mock import Mock, call + +import pytest +from freezegun import freeze_time + +from notifications_utils.clients.redis.redis_client import RedisClient, prepare_value + + +@pytest.fixture(scope="function") +def mocked_redis_pipeline(): + return Mock() + + +@pytest.fixture +def delete_mock(): + return Mock(return_value=4) + + +@pytest.fixture(scope="function") +def mocked_redis_client(app, mocked_redis_pipeline, delete_mock, mocker): + app.config["REDIS_ENABLED"] = True + + redis_client = RedisClient() + redis_client.init_app(app) + + mocker.patch.object(redis_client.redis_store, "get", return_value=100) + mocker.patch.object(redis_client.redis_store, "set") + mocker.patch.object(redis_client.redis_store, "incr") + mocker.patch.object(redis_client.redis_store, "delete") + mocker.patch.object( + redis_client.redis_store, "pipeline", return_value=mocked_redis_pipeline + ) + + mocker.patch.object( + redis_client, "scripts", {"delete-keys-by-pattern": delete_mock} + ) + + mocker.patch.object( + redis_client.redis_store, + "hgetall", + return_value={b"template-1111": b"8", b"template-2222": b"8"}, + ) + + return redis_client + + +@pytest.fixture +def failing_redis_client(mocked_redis_client, delete_mock): + mocked_redis_client.redis_store.get.side_effect = Exception("get failed") + mocked_redis_client.redis_store.set.side_effect = Exception("set failed") + mocked_redis_client.redis_store.incr.side_effect = Exception("incr failed") + mocked_redis_client.redis_store.pipeline.side_effect = Exception("pipeline failed") + mocked_redis_client.redis_store.delete.side_effect = Exception("delete failed") + delete_mock.side_effect = Exception("delete by pattern failed") + return mocked_redis_client + + +def test_should_not_raise_exception_if_raise_set_to_false( + app, caplog, failing_redis_client, mocker +): + mock_logger = mocker.patch("flask.Flask.logger") + + assert failing_redis_client.get("get_key") is None + assert failing_redis_client.set("set_key", "set_value") is None + assert failing_redis_client.incr("incr_key") is None + assert failing_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False + assert failing_redis_client.delete("delete_key") is None + assert failing_redis_client.delete("a", "b", "c") is None + assert failing_redis_client.delete_by_pattern("pattern") == 0 + + assert mock_logger.mock_calls == [ + call.exception("Redis error performing get on get_key"), + call.exception("Redis error performing set on set_key"), + call.exception("Redis error performing incr on incr_key"), + call.exception("Redis error performing rate-limit-pipeline on rate_limit_key"), + call.exception("Redis error performing delete on delete_key"), + call.exception("Redis error performing delete on a, b, c"), + call.exception("Redis error performing delete-by-pattern on pattern"), + ] + + +def test_should_raise_exception_if_raise_set_to_true( + app, + failing_redis_client, +): + with pytest.raises(Exception) as e: + failing_redis_client.get("test", raise_exception=True) + assert str(e.value) == "get failed" + + with pytest.raises(Exception) as e: + failing_redis_client.set("test", "test", raise_exception=True) + assert str(e.value) == "set failed" + + with pytest.raises(Exception) as e: + failing_redis_client.incr("test", raise_exception=True) + assert str(e.value) == "incr failed" + + with pytest.raises(Exception) as e: + failing_redis_client.exceeded_rate_limit("test", 100, 200, raise_exception=True) + assert str(e.value) == "pipeline failed" + + with pytest.raises(Exception) as e: + failing_redis_client.delete("test", raise_exception=True) + assert str(e.value) == "delete failed" + + with pytest.raises(Exception) as e: + failing_redis_client.delete_by_pattern("pattern", raise_exception=True) + assert str(e.value) == "delete by pattern failed" + + +def test_should_not_call_if_not_enabled(mocked_redis_client, delete_mock): + mocked_redis_client.active = False + + assert mocked_redis_client.get("get_key") is None + assert mocked_redis_client.set("set_key", "set_value") is None + assert mocked_redis_client.incr("incr_key") is None + assert mocked_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False + assert mocked_redis_client.delete("delete_key") is None + assert mocked_redis_client.delete_by_pattern("pattern") == 0 + + mocked_redis_client.redis_store.get.assert_not_called() + mocked_redis_client.redis_store.set.assert_not_called() + mocked_redis_client.redis_store.incr.assert_not_called() + mocked_redis_client.redis_store.delete.assert_not_called() + mocked_redis_client.redis_store.pipeline.assert_not_called() + delete_mock.assert_not_called() + + +def test_should_call_set_if_enabled(mocked_redis_client): + mocked_redis_client.set("key", "value") + mocked_redis_client.redis_store.set.assert_called_with( + "key", "value", None, None, False, False + ) + + +def test_should_call_get_if_enabled(mocked_redis_client): + assert mocked_redis_client.get("key") == 100 + mocked_redis_client.redis_store.get.assert_called_with("key") + + +@freeze_time("2001-01-01 12:00:00.000000") +def test_exceeded_rate_limit_should_add_correct_calls_to_the_pipe( + mocked_redis_client, mocked_redis_pipeline +): + mocked_redis_client.exceeded_rate_limit("key", 100, 100) + assert mocked_redis_client.redis_store.pipeline.called + mocked_redis_pipeline.zadd.assert_called_with("key", {978350400.0: 978350400.0}) + mocked_redis_pipeline.zremrangebyscore.assert_called_with( + "key", "-inf", 978350300.0 + ) + mocked_redis_pipeline.zcard.assert_called_with("key") + mocked_redis_pipeline.expire.assert_called_with("key", 100) + assert mocked_redis_pipeline.execute.called + + +@freeze_time("2001-01-01 12:00:00.000000") +def test_exceeded_rate_limit_should_fail_request_if_over_limit( + mocked_redis_client, mocked_redis_pipeline +): + mocked_redis_pipeline.execute.return_value = [True, True, 100, True] + assert mocked_redis_client.exceeded_rate_limit("key", 99, 100) + + +@freeze_time("2001-01-01 12:00:00.000000") +def test_exceeded_rate_limit_should_allow_request_if_not_over_limit( + mocked_redis_client, mocked_redis_pipeline +): + mocked_redis_pipeline.execute.return_value = [True, True, 100, True] + assert not mocked_redis_client.exceeded_rate_limit("key", 101, 100) + + +@freeze_time("2001-01-01 12:00:00.000000") +def test_exceeded_rate_limit_not_exceeded(mocked_redis_client, mocked_redis_pipeline): + mocked_redis_pipeline.execute.return_value = [True, True, 80, True] + assert not mocked_redis_client.exceeded_rate_limit("key", 90, 100) + + +def test_exceeded_rate_limit_should_not_call_if_not_enabled( + mocked_redis_client, mocked_redis_pipeline +): + mocked_redis_client.active = False + + assert not mocked_redis_client.exceeded_rate_limit("key", 100, 100) + assert not mocked_redis_client.redis_store.pipeline.called + + +def test_delete(mocked_redis_client): + key = "hash-key" + mocked_redis_client.delete(key) + mocked_redis_client.redis_store.delete.assert_called_with(key) + + +def test_delete_multi(mocked_redis_client): + mocked_redis_client.delete("a", "b", "c") + mocked_redis_client.redis_store.delete.assert_called_with("a", "b", "c") + + +@pytest.mark.parametrize( + "input,output", + [ + (b"asdf", b"asdf"), + ("asdf", "asdf"), + (0, 0), + (1.2, 1.2), + (uuid.UUID(int=0), "00000000-0000-0000-0000-000000000000"), + pytest.param({"a": 1}, None, marks=pytest.mark.xfail(raises=ValueError)), + pytest.param( + datetime.utcnow(), None, marks=pytest.mark.xfail(raises=ValueError) + ), + ], +) +def test_prepare_value(input, output): + assert prepare_value(input) == output + + +def test_delete_by_pattern(mocked_redis_client, delete_mock): + ret = mocked_redis_client.delete_by_pattern("foo") + assert ret == 4 + delete_mock.assert_called_once_with(args=["foo"]) diff --git a/tests/notification_utils/clients/redis/test_request_cache.py b/tests/notification_utils/clients/redis/test_request_cache.py new file mode 100644 index 000000000..876c4a976 --- /dev/null +++ b/tests/notification_utils/clients/redis/test_request_cache.py @@ -0,0 +1,190 @@ +import pytest + +from notifications_utils.clients.redis import RequestCache +from notifications_utils.clients.redis.redis_client import RedisClient + + +@pytest.fixture(scope="function") +def mocked_redis_client(app): + app.config["REDIS_ENABLED"] = True + redis_client = RedisClient() + redis_client.init_app(app) + return redis_client + + +@pytest.fixture +def cache(mocked_redis_client): + return RequestCache(mocked_redis_client) + + +@pytest.mark.parametrize( + "args, kwargs, expected_cache_key", + ( + ([1, 2, 3], {}, "1-2-3-None-None-None"), + ([1, 2, 3, 4, 5, 6], {}, "1-2-3-4-5-6"), + ([1, 2, 3], {"x": 4, "y": 5, "z": 6}, "1-2-3-4-5-6"), + ([1, 2, 3, 4], {"y": 5}, "1-2-3-4-5-None"), + ), +) +def test_set( + mocker, + mocked_redis_client, + cache, + args, + kwargs, + expected_cache_key, +): + mock_redis_set = mocker.patch.object( + mocked_redis_client, + "set", + ) + mock_redis_get = mocker.patch.object( + mocked_redis_client, + "get", + return_value=None, + ) + + @cache.set("{a}-{b}-{c}-{x}-{y}-{z}") + def foo(a, b, c, x=None, y=None, z=None): + return "bar" + + assert foo(*args, **kwargs) == "bar" + + mock_redis_get.assert_called_once_with(expected_cache_key) + + mock_redis_set.assert_called_once_with( + expected_cache_key, + '"bar"', + ex=604_800, + ) + + +@pytest.mark.parametrize( + "cache_set_call, expected_redis_client_ttl", + ( + (0, 0), + (1, 1), + (1.111, 1), + ("2000", 2_000), + ), +) +def test_set_with_custom_ttl( + mocker, + mocked_redis_client, + cache, + cache_set_call, + expected_redis_client_ttl, +): + mock_redis_set = mocker.patch.object( + mocked_redis_client, + "set", + ) + mocker.patch.object( + mocked_redis_client, + "get", + return_value=None, + ) + + @cache.set("foo", ttl_in_seconds=cache_set_call) + def foo(): + return "bar" + + foo() + + mock_redis_set.assert_called_once_with( + "foo", + '"bar"', + ex=expected_redis_client_ttl, + ) + + +def test_raises_if_key_doesnt_match_arguments(cache): + @cache.set("{baz}") + def foo(bar): + pass + + with pytest.raises(KeyError): + foo(1) + + with pytest.raises(KeyError): + foo() + + +def test_get(mocker, mocked_redis_client, cache): + mock_redis_get = mocker.patch.object( + mocked_redis_client, + "get", + return_value=b'"bar"', + ) + + @cache.set("{a}-{b}-{c}") + def foo(a, b, c): + # This function should not be called because the cache has + # returned a value + raise RuntimeError + + assert foo(1, 2, 3) == "bar" + + mock_redis_get.assert_called_once_with("1-2-3") + + +def test_delete(mocker, mocked_redis_client, cache): + mock_redis_delete = mocker.patch.object( + mocked_redis_client, + "delete", + ) + + @cache.delete("{a}-{b}-{c}") + def foo(a, b, c): + return "bar" + + assert foo(1, 2, 3) == "bar" + + mock_redis_delete.assert_called_once_with("1-2-3") + + +def test_delete_even_if_call_raises(mocker, mocked_redis_client, cache): + mock_redis_delete = mocker.patch.object( + mocked_redis_client, + "delete", + ) + + @cache.delete("bar") + def foo(): + raise RuntimeError + + with pytest.raises(RuntimeError): + foo() + + mock_redis_delete.assert_called_once_with("bar") + + +def test_delete_by_pattern(mocker, mocked_redis_client, cache): + mock_redis_delete = mocker.patch.object( + mocked_redis_client, + "delete_by_pattern", + ) + + @cache.delete_by_pattern("{a}-{b}-{c}-???") + def foo(a, b, c): + return "bar" + + assert foo(1, 2, 3) == "bar" + + mock_redis_delete.assert_called_once_with("1-2-3-???") + + +def test_delete_by_pattern_even_if_call_raises(mocker, mocked_redis_client, cache): + mock_redis_delete = mocker.patch.object( + mocked_redis_client, + "delete_by_pattern", + ) + + @cache.delete_by_pattern("bar-???") + def foo(): + raise RuntimeError + + with pytest.raises(RuntimeError): + foo() + + mock_redis_delete.assert_called_once_with("bar-???") diff --git a/tests/notification_utils/clients/test_redis.py b/tests/notification_utils/clients/test_redis.py new file mode 100644 index 000000000..5d813b938 --- /dev/null +++ b/tests/notification_utils/clients/test_redis.py @@ -0,0 +1,7 @@ +from notifications_utils.clients.redis import rate_limit_cache_key + + +def test_rate_limit_cache_key(sample_service): + assert rate_limit_cache_key(sample_service.id, "TEST") == "{}-TEST".format( + sample_service.id + ) diff --git a/tests/notification_utils/clients/zendesk/test_zendesk_client.py b/tests/notification_utils/clients/zendesk/test_zendesk_client.py new file mode 100644 index 000000000..be19c9a89 --- /dev/null +++ b/tests/notification_utils/clients/zendesk/test_zendesk_client.py @@ -0,0 +1,220 @@ +from base64 import b64decode + +import pytest + +from notifications_utils.clients.zendesk.zendesk_client import ( + NotifySupportTicket, + ZendeskClient, + ZendeskError, +) + + +@pytest.fixture(scope="function") +def zendesk_client(app): + client = ZendeskClient() + + app.config["ZENDESK_API_KEY"] = "testkey" + + client.init_app(app) + + return client + + +def test_zendesk_client_send_ticket_to_zendesk(zendesk_client, app, mocker, rmock): + rmock.request( + "POST", + ZendeskClient.ZENDESK_TICKET_URL, + status_code=201, + json={ + "ticket": { + "id": 12345, + "subject": "Something is wrong", + } + }, + ) + mock_logger = mocker.patch.object(app.logger, "info") + + ticket = NotifySupportTicket("subject", "message", "incident") + zendesk_client.send_ticket_to_zendesk(ticket) + + assert rmock.last_request.headers["Authorization"][:6] == "Basic " + b64_auth = rmock.last_request.headers["Authorization"][6:] + assert ( + b64decode(b64_auth.encode()).decode() + == "zd-api-notify@digital.cabinet-office.gov.uk/token:testkey" + ) + assert rmock.last_request.json() == ticket.request_data + mock_logger.assert_called_once_with("Zendesk create ticket 12345 succeeded") + + +def test_zendesk_client_send_ticket_to_zendesk_error( + zendesk_client, app, mocker, rmock +): + rmock.request( + "POST", ZendeskClient.ZENDESK_TICKET_URL, status_code=401, json={"foo": "bar"} + ) + + mock_logger = mocker.patch.object(app.logger, "error") + + ticket = NotifySupportTicket("subject", "message", "incident") + + with pytest.raises(ZendeskError): + zendesk_client.send_ticket_to_zendesk(ticket) + + mock_logger.assert_called_with( + "Zendesk create ticket request failed with 401 '{'foo': 'bar'}'" + ) + + +@pytest.mark.parametrize( + "p1_arg, expected_tags, expected_priority", + ( + ( + {}, + ["govuk_notify_support"], + "normal", + ), + ( + { + "p1": False, + }, + ["govuk_notify_support"], + "normal", + ), + ( + { + "p1": True, + }, + ["govuk_notify_emergency"], + "urgent", + ), + ), +) +def test_notify_support_ticket_request_data(p1_arg, expected_tags, expected_priority): + notify_ticket_form = NotifySupportTicket("subject", "message", "question", **p1_arg) + + assert notify_ticket_form.request_data == { + "ticket": { + "subject": "subject", + "comment": { + "body": "message", + "public": True, + }, + "group_id": NotifySupportTicket.NOTIFY_GROUP_ID, + "organization_id": NotifySupportTicket.NOTIFY_ORG_ID, + "ticket_form_id": NotifySupportTicket.NOTIFY_TICKET_FORM_ID, + "priority": expected_priority, + "tags": expected_tags, + "type": "question", + "custom_fields": [ + {"id": "1900000744994", "value": "notify_ticket_type_non_technical"}, + {"id": "360022836500", "value": []}, + {"id": "360022943959", "value": None}, + {"id": "360022943979", "value": None}, + {"id": "1900000745014", "value": None}, + ], + } + } + + +def test_notify_support_ticket_request_data_with_message_hidden_from_requester(): + notify_ticket_form = NotifySupportTicket( + "subject", "message", "problem", requester_sees_message_content=False + ) + + assert notify_ticket_form.request_data["ticket"]["comment"]["public"] is False + + +@pytest.mark.parametrize( + "name, zendesk_name", [("Name", "Name"), (None, "(no name supplied)")] +) +def test_notify_support_ticket_request_data_with_user_name_and_email( + name, zendesk_name +): + notify_ticket_form = NotifySupportTicket( + "subject", "message", "question", user_name=name, user_email="user@example.com" + ) + + assert ( + notify_ticket_form.request_data["ticket"]["requester"]["email"] + == "user@example.com" + ) + assert ( + notify_ticket_form.request_data["ticket"]["requester"]["name"] == zendesk_name + ) + + +@pytest.mark.parametrize( + "custom_fields, tech_ticket_tag, categories, org_id, org_type, service_id", + [ + ( + {"technical_ticket": True}, + "notify_ticket_type_technical", + [], + None, + None, + None, + ), + ( + {"technical_ticket": False}, + "notify_ticket_type_non_technical", + [], + None, + None, + None, + ), + ( + {"ticket_categories": ["notify_billing", "notify_bug"]}, + "notify_ticket_type_non_technical", + ["notify_billing", "notify_bug"], + None, + None, + None, + ), + ( + {"org_id": "1234", "org_type": "local"}, + "notify_ticket_type_non_technical", + [], + "1234", + "notify_org_type_local", + None, + ), + ( + {"service_id": "abcd", "org_type": "nhs"}, + "notify_ticket_type_non_technical", + [], + None, + "notify_org_type_nhs", + "abcd", + ), + ], +) +def test_notify_support_ticket_request_data_custom_fields( + custom_fields, + tech_ticket_tag, + categories, + org_id, + org_type, + service_id, +): + notify_ticket_form = NotifySupportTicket( + "subject", "message", "question", **custom_fields + ) + + assert notify_ticket_form.request_data["ticket"]["custom_fields"] == [ + {"id": "1900000744994", "value": tech_ticket_tag}, + {"id": "360022836500", "value": categories}, + {"id": "360022943959", "value": org_id}, + {"id": "360022943979", "value": org_type}, + {"id": "1900000745014", "value": service_id}, + ] + + +def test_notify_support_ticket_request_data_email_ccs(): + notify_ticket_form = NotifySupportTicket( + "subject", "message", "question", email_ccs=["someone@example.com"] + ) + + assert notify_ticket_form.request_data["ticket"]["email_ccs"] == [ + {"user_email": "someone@example.com", "action": "put"}, + ] diff --git a/tests/notification_utils/conftest.py b/tests/notification_utils/conftest.py new file mode 100644 index 000000000..7150b1486 --- /dev/null +++ b/tests/notification_utils/conftest.py @@ -0,0 +1,45 @@ +import pytest +import requests_mock +from flask import Flask + +from notifications_utils import request_helper + + +class FakeService: + id = "1234" + + +@pytest.fixture +def app(): + flask_app = Flask(__name__) + ctx = flask_app.app_context() + ctx.push() + + yield flask_app + + ctx.pop() + + +@pytest.fixture +def celery_app(mocker): + app = Flask(__name__) + app.config["CELERY"] = {"broker_url": "foo"} + app.config["NOTIFY_TRACE_ID_HEADER"] = "Ex-Notify-Request-Id" + request_helper.init_app(app) + + ctx = app.app_context() + ctx.push() + + yield app + ctx.pop() + + +@pytest.fixture(scope="session") +def sample_service(): + return FakeService() + + +@pytest.fixture +def rmock(): + with requests_mock.mock() as rmock: + yield rmock diff --git a/tests/notification_utils/country_synonyms.py b/tests/notification_utils/country_synonyms.py new file mode 100644 index 000000000..0c035a4c2 --- /dev/null +++ b/tests/notification_utils/country_synonyms.py @@ -0,0 +1,1937 @@ +ALL = ( + ("al'iraq", "Iraq"), + ("aljazā'ir", "Algeria"), + ("amelikahuipu'ia", "United States"), + ("cote d'ivoire", "Ivory Coast"), + ("coted'ivoire", "Ivory Coast"), + ("democratic people's republic of koread", "North Korea"), + ("democraticpeople'srepublicofkoread", "North Korea"), + ("ityop'ia", "Ethiopia"), + ("lao people's democratic republic", "Laos"), + ("laopeople'sdemocraticrepublic", "Laos"), + ("o'zbekstan", "Uzbekistan"), + ("people's democratic republic of algeriaalgerie", "Algeria"), + ("people's republic of bangladesh", "Bangladesh"), + ("people's republic of china", "China"), + ("people'sdemocraticrepublicofalgeriaalgerie", "Algeria"), + ("people'srepublicofbangladesh", "Bangladesh"), + ("people'srepublicofchina", "China"), + ("republic of cote d'ivoire", "Ivory Coast"), + ("republicofcoted'ivoire", "Ivory Coast"), + ("sak'art'velo", "Georgia"), + ("socialist people's libyan arab", "Libya"), + ("socialistpeople'slibyanarab", "Libya"), + ("the democratic people's republic of korea", "North Korea"), + ("the lao people's democratic republic", "Laos"), + ("the people's democratic republic of algeria", "Algeria"), + ("the people's republic of bangladesh", "Bangladesh"), + ("the people's republic of china", "China"), + ("the republic of cote d'ivoire", "Ivory Coast"), + ("thedemocraticpeople'srepublicofkorea", "North Korea"), + ("thelaopeople'sdemocraticrepublic", "Laos"), + ("thepeople'sdemocraticrepublicofalgeria", "Algeria"), + ("thepeople'srepublicofbangladesh", "Bangladesh"), + ("thepeople'srepublicofchina", "China"), + ("therepublicofcoted'ivoire", "Ivory Coast"), + ("timor lorosa'e", "East Timor"), + ("timorlorosa'e", "East Timor"), + ("yisra'el", "Israel"), + ("\u200e", "Iraq"), + ("aaland", "Åland Islands"), + ("abudhabi", "Abu Dhabi"), + ("abyssinia", "Ethiopia"), + ("ad", "Andorra"), + ("ae", "United Arab Emirates"), + ("aeaj", "Ajman"), + ("aeaz", "Abu Dhabi"), + ("aedu", "Dubai"), + ("aefu", "Fujairah"), + ("aerk", "Ras al-Khaimah"), + ("aeroes", "Faroe Islands"), + ("aesh", "Sharjah"), + ("aeuq", "Umm al-Quwain"), + ("af", "Afghanistan"), + ("afganastan", "Afghanistan"), + ("afganestan", "Afghanistan"), + ("afganhistan", "Afghanistan"), + ("afganistan", "Afghanistan"), + ("afghanestan", "Afghanistan"), + ("afghanlstan", "Afghanistan"), + ("afghistan", "Afghanistan"), + ("aforika borwa", "South Africa"), + ("aforikaborwa", "South Africa"), + ("afrika borwa", "South Africa"), + ("afrika dzonga", "South Africa"), + ("afrikaborwa", "South Africa"), + ("afrikadzonga", "South Africa"), + ("afurika tshipembe", "South Africa"), + ("afurikatshipembe", "South Africa"), + ("ag", "Antigua and Barbuda"), + ("agawec", "Mauritania"), + ("ahvenanmaa", "Åland Islands"), + ("ai", "Anguilla"), + ("aigeria", "Algeria"), + ("al itihaad al islamiya", "Somalia"), + ("al", "Albania"), + ("alabnia", "Albania"), + ("aland", "Åland Islands"), + ("albana", "Albania"), + ("albanian", "Albania"), + ("albanija", "Albania"), + ("albaá¸Ĩrayn", "Bahrain"), + ("albenia", "Albania"), + ("albiana", "Albania"), + ("alegeria", "Algeria"), + ("algeir", "Algeria"), + ("algeirs", "Algeria"), + ("algers", "Algeria"), + ("algieria", "Algeria"), + ("algiers", "Algeria"), + ("alibania", "Albania"), + ("aliraq", "Iraq"), + ("alitihaad alislamiya", "Somalia"), + ("alitihaadalislamiya", "Somalia"), + ("almamlaka al‘arabiyyah as sa‘ÅĢdiyyah", "Saudi Arabia"), + ("almamlakaal‘arabiyyahassa‘ÅĢdiyyah", "Saudi Arabia"), + ("almaÉŖrÊb", "Morocco"), + ("alyaman", "Yemen"), + ("al’imārat al‘arabiyyah almuttaá¸Ĩidah", "United Arab Emirates"), + ("al’imāratal‘arabiyyahalmuttaá¸Ĩidah", "United Arab Emirates"), + ("al’urdun", "Jordan"), + ("am", "Armenia"), + ("ameeri", "United States"), + ("ameica", "United States"), + ("amerca", "United States"), + ("amercia", "United States"), + ("ameria", "United States"), + ("america", "United States"), + ("american virgin islands", "United States Virgin Islands"), + ("americansamoa", "American Samoa"), + ("americanvirginislands", "United States Virgin Islands"), + ("americia", "United States"), + ("amerika sāmoa", "Samoa"), + ("amerikasāmoa", "Samoa"), + ("amerruk", "Morocco"), + ("amrica", "United States"), + ("angolo", "Angola"), + ("anmerica", "United States"), + ("annam", "Vietnam"), + ("antiguaandbarbuda", "Antigua and Barbuda"), + ("ao", "Angola"), + ("aorōkin m˧ajeÄŧ", "Marshall Islands"), + ("aorōkinm˧ajeÄŧ", "Marshall Islands"), + ("aotearoa", "New Zealand"), + ("aq", "Antarctica"), + ("ar", "Argentina"), + ("arab emir ates", "United Arab Emirates"), + ("arab emirates", "United Arab Emirates"), + ("arab republic of egypt", "Egypt"), + ("arabemirates", "United Arab Emirates"), + ("arabrepublicofegypt", "Egypt"), + ("argenina", "Argentina"), + ("argentiha", "Argentina"), + ("argentine republic", "Argentina"), + ("argentine", "Argentina"), + ("argentinerepublic", "Argentina"), + ("argentinia", "Argentina"), + ("argentna", "Argentina"), + ("arima", "Armenia"), + ("arminia", "Armenia"), + ("as", "American Samoa"), + ("ascension island", "Ascension"), + ("ascensionisland", "Ascension"), + ("assudan", "Sudan"), + ("at", "Austria"), + ("au", "Australia"), + ("ausralia", "Australia"), + ("austalia", "Australia"), + ("austraila", "Australia"), + ("austrailia", "Australia"), + ("australa", "Australia"), + ("australla", "Australia"), + ("austrilia", "Australia"), + ("austrlia", "Australia"), + ("autralia", "Australia"), + ("avstralia", "Australia"), + ("avstria", "Australia"), + ("aw", "Aruba"), + ("ax", "Åland Islands"), + ("ayiti", "Haiti"), + ("az", "Azerbaijan"), + ("azebaijan", "Azerbaijan"), + ("azeraijan", "Azerbaijan"), + ("azerbaijani republic", "Azerbaijan"), + ("azerbaijanirepublic", "Azerbaijan"), + ("azerbaijann", "Azerbaijan"), + ("azerbaisan", "Azerbaijan"), + ("azerbaizan", "Azerbaijan"), + ("azerbajan", "Azerbaijan"), + ("azerbajdzhan republic", "Azerbaijan"), + ("azerbajdzhan", "Azerbaijan"), + ("azerbajdzhanrepublic", "Azerbaijan"), + ("azerbaycan", "Azerbaijan"), + ("azerbayjan", "Azerbaijan"), + ("azerbeyjan", "Azerbaijan"), + ("azerbpijan", "Azerbaijan"), + ("azərbaycan", "Azerbaijan"), + ("aşşÅĢmāl", "Somalia"), + ("ba", "Bosnia and Herzegovina"), + ("bailiwick of guernsey", "United Kingdom"), + ("bailiwick of jersey", "United Kingdom"), + ("bailiwickofguernsey", "United Kingdom"), + ("bailiwickofjersey", "United Kingdom"), + ("bakerisland", "Baker Island"), + ("bangla desh", "Bangladesh"), + ("basutoland", "Lesotho"), + ("bat", "British Antarctic Territory"), + ("bb", "Barbados"), + ("bd", "Bangladesh"), + ("be", "Belgium"), + ("belau", "Palau"), + ("belgie", "Belgium"), + ("belgien", "Belgium"), + ("belgique", "Belgium"), + ("belgiÃĢ", "Belgium"), + ("belguim", "Belgium"), + ("bermudas", "Bermuda"), + ("bf", "Burkina Faso"), + ("bg", "Bulgaria"), + ("bh", "Bahrain"), + ("bharat", "India"), + ("bharôt", "India"), + ("bharôtô", "India"), + ("bhārat", "India"), + ("bhārata", "India"), + ("bhāratadēsam", "India"), + ("bhāratam", "India"), + ("bi", "Burundi"), + ("bielaruś", "Belarus"), + ("bih", "Bosnia and Herzegovina"), + ("bj", "Benin"), + ("bl", "Saint BarthÊlemy"), + ("bm", "Bermuda"), + ("bn", "Brunei"), + ("bo", "Bolivia"), + ("bolivarian republic of venezuela", "Venezuela"), + ("bolivarianrepublicofvenezuela", "Venezuela"), + ("bosna i hercegovina", "Bosnia and Herzegovina"), + ("bosnaihercegovina", "Bosnia and Herzegovina"), + ("bosniaandherzegovina", "Bosnia and Herzegovina"), + ("bosniaherzegovina", "Bosnia and Herzegovina"), + ("bouvetisland", "Bouvet Island"), + ("bqbo", "Bonaire"), + ("bqsa", "Saba"), + ("bqse", "Sint Eustatius"), + ("br", "Brazil"), + ("brasil", "Brazil"), + ("brazzaville", "Congo"), + ("british guiana", "Guyana"), + ("british honduras", "Belize"), + ("britishantarcticterritory", "British Antarctic Territory"), + ("britishguiana", "Guyana"), + ("britishhonduras", "Belize"), + ("britishindianoceanterritory", "British Indian Ocean Territory"), + ("britishvirginislands", "British Virgin Islands"), + ("brunei darussalam", "Brunei"), + ("bruneidarussalam", "Brunei"), + ("bs", "The Bahamas"), + ("bt", "Bhutan"), + ("bugaria", "Bulgaria"), + ("bukchosŏn", "North Korea"), + ("bulagar", "Bulgaria"), + ("bulgariya", "Bulgaria"), + ("buliwya", "Bolivia"), + ("bundesrepublik", "Germany"), + ("burkina fasoupper", "Burkina Faso"), + ("burkinafaso", "Burkina Faso"), + ("burkinafasoupper", "Burkina Faso"), + ("bv", "Bouvet Island"), + ("bvi", "British Virgin Islands"), + ("bw", "Botswana"), + ("by", "Belarus"), + ("byelarus", "Belarus"), + ("byelorussia", "Belarus"), + ("bz", "Belize"), + ("bārata", "India"), + ("bălgarija", "Bulgaria"), + ("ca", "Canada"), + ("cabo verde", "Cape Verde"), + ("cabo", "Cape Verde"), + ("caboverde", "Cape Verde"), + ("cameroun", "Cameroon"), + ("canadaigua", "Canada"), + ("candada", "Canada"), + ("capeverde", "Cape Verde"), + ("car", "Central African Republic"), + ("cathay", "China"), + ("caymanislands", "Cayman Islands"), + ("cc", "Cocos (Keeling) Islands"), + ("cd", "Congo (Democratic Republic)"), + ("central africa", "Central African Republic"), + ("centralafrica", "Central African Republic"), + ("centralafricanrepublic", "Central African Republic"), + ("ceska", "Czechia"), + ("ceylon", "Sri Lanka"), + ("cf", "Central African Republic"), + ("cg", "Congo"), + ("ch", "Switzerland"), + ("chinese taipei", "Taiwan"), + ("chinesetaipei", "Taiwan"), + ("christmasisland", "Christmas Island"), + ("ci", "Ivory Coast"), + ("citta del vaticano", "Vatican City"), + ("cittadelvaticano", "Vatican City"), + ("ck", "Cook Islands"), + ("cl", "Chile"), + ("cm", "Cameroon"), + ("cn", "China"), + ("co", "Colombia"), + ("coasta rica", "Costa Rica"), + ("coastarica", "Costa Rica"), + ("cocos(keeling)islands", "Cocos (Keeling) Islands"), + ("collectivity of saint martin", "Saint-Martin (French part)"), + ("collectivityofsaintmartin", "Saint-Martin (French part)"), + ("commonwealth of australia", "Australia"), + ("commonwealth of bahamas", "The Bahamas"), + ("commonwealth of dominica", "Dominica"), + ("commonwealth of puerto rico", "Puerto Rico"), + ("commonwealth of the northern mariana islands", "Northern Mariana Islands"), + ("commonwealthofaustralia", "Australia"), + ("commonwealthofbahamas", "The Bahamas"), + ("commonwealthofdominica", "Dominica"), + ("commonwealthofpuertorico", "Puerto Rico"), + ("commonwealthofthenorthernmarianaislands", "Northern Mariana Islands"), + ("comores", "Comoros"), + ("congo(democraticrepublic)", "Congo (Democratic Republic)"), + ("congobrazzaville", "Congo (Democratic Republic)"), + ("cookislands", "Cook Islands"), + ("cooperative republic of guyana", "Guyana"), + ("cooperativerepublicofguyana", "Guyana"), + ("costa rico", "Costa Rica"), + ("costarica", "Costa Rica"), + ("costarico", "Costa Rica"), + ("cote divoire", "Ivory Coast"), + ("cotedivoire", "Ivory Coast"), + ("country of curaçao", "Curaçao"), + ("countryofcuraçao", "Curaçao"), + ("cr", "Costa Rica"), + ("crna gora", "Montenegro"), + ("crnagora", "Montenegro"), + ("cs", "Czechia"), + ("cu", "Cuba"), + ("curacao", "Curaçao"), + ("cv", "Cape Verde"), + ("cw", "Curaçao"), + ("cx", "Christmas Island"), + ("cy", "Cyprus"), + ("cz", "Czechia"), + ("czech republic", "Czechia"), + ("czechoslav", "Czechia"), + ("czechoslovak republic", "Czechia"), + ("czechoslovakrepublic", "Czechia"), + ("czechrepublic", "Czechia"), + ("dahomey", "Benin"), + ("danmark", "Denmark"), + ("dawlat ulkuwayt", "Kuwait"), + ("dawlatulkuwayt", "Kuwait"), + ("dd", "Germany"), + ("de", "Germany"), + ("democratic republic of sao tome and principe", "Sao Tome and Principe"), + ("democratic republic of the congo", "Congo (Democratic Republic)"), + ("democratic republic of timorlestetimor", "East Timor"), + ("democratic socialist republic of sri lanka", "Sri Lanka"), + ("democraticrepublicofsaotomeandprincipe", "Sao Tome and Principe"), + ("democraticrepublicofthecongo", "Congo (Democratic Republic)"), + ("democraticrepublicoftimorlestetimor", "East Timor"), + ("democraticsocialistrepublicofsrilanka", "Sri Lanka"), + ("deutschland", "Germany"), + ("dhivehi raajje", "Maldives"), + ("dhivehiraajje", "Maldives"), + ("dj", "Djibouti"), + ("dk", "Denmark"), + ("dm", "Dominica"), + ("do", "Dominican Republic"), + ("dominicanrepublic", "Dominican Republic"), + ("dominique", "Dominica"), + ("dprk", "North Korea"), + ("druk yul", "Bhutan"), + ("drukyul", "Bhutan"), + ("ducie and oeno islands", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ("ducieandoenoislands", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ("dutch east indies", "Indonesia"), + ("dutcheastindies", "Indonesia"), + ("dz", "Algeria"), + ("dzayer", "Algeria"), + ("e civitate vaticana", "Vatican City"), + ("east pakistan", "Bangladesh"), + ("eastern samoa", "American Samoa"), + ("easternsamoa", "American Samoa"), + ("eastgermany", "Germany"), + ("eastpakistan", "Bangladesh"), + ("easttimor", "East Timor"), + ("ec", "Ecuador"), + ("ecivitatevaticana", "Vatican City"), + ("ee", "Estonia"), + ("eesti", "Estonia"), + ("eg", "Egypt"), + ("egpyt", "Egypt"), + ("egyot", "Egypt"), + ("egyt", "Egypt"), + ("eh", "Western Sahara"), + ("eire", "Ireland"), + ("ellada", "Greece"), + ("ellan vannin", "United Kingdom"), + ("ellanvannin", "United Kingdom"), + ("ellas", "Greece"), + ("ellice islands", "Tuvalu"), + ("elliceislands", "Tuvalu"), + ("elmeÉŖrib", "Morocco"), + ("elsalvador", "El Salvador"), + ("emirate of abu dhabi", "Abu Dhabi"), + ("emirate of ajman", "Ajman"), + ("emirate of dubai", "Dubai"), + ("emirate of fujairah", "Fujairah"), + ("emirate of ras alkhaimah", "Ras al-Khaimah"), + ("emirate of sharjah", "Sharjah"), + ("emirate of umm alquwain", "Umm al-Quwain"), + ("emirateofabudhabi", "Abu Dhabi"), + ("emirateofajman", "Ajman"), + ("emirateofdubai", "Dubai"), + ("emirateoffujairah", "Fujairah"), + ("emirateofrasalkhaimah", "Ras al-Khaimah"), + ("emirateofsharjah", "Sharjah"), + ("emirateofummalquwain", "Umm al-Quwain"), + ("equatorialguinea", "Equatorial Guinea"), + ("er", "Eritrea"), + ("ertra", "Eritrea"), + ("es", "Spain"), + ("esce", "Ceuta"), + ("esml", "Melilla"), + ("espainia", "Spain"), + ("espanha", "Spain"), + ("espanya", "Spain"), + ("espaÃąa", "Spain"), + ("estados unidos", "United States"), + ("estadosunidos", "United States"), + ("esthonia", "Estonia"), + ("et", "Ethiopia"), + ("ethopi", "Ethiopia"), + ("falklandislands", "Falkland Islands"), + ("faroeislands", "Faroe Islands"), + ("faroes", "Faroe Islands"), + ("federal democratic republic of ethiopia", "Ethiopia"), + ("federal democratic republic of nepal", "Nepal"), + ("federal islamic republic of the comoros", "Comoros"), + ("federal republic of germany", "Germany"), + ("federal republic of nigeria", "Nigeria"), + ("federal republic of somalia", "Somalia"), + ("federal republic of somaliaaiai", "Somalia"), + ("federaldemocraticrepublicofethiopia", "Ethiopia"), + ("federaldemocraticrepublicofnepal", "Nepal"), + ("federalislamicrepublicofthecomoros", "Comoros"), + ("federalrepublicofgermany", "Germany"), + ("federalrepublicofnigeria", "Nigeria"), + ("federalrepublicofsomalia", "Somalia"), + ("federalrepublicofsomaliaaiai", "Somalia"), + ("federated states of micronesia", "Micronesia"), + ("federatedstatesofmicronesia", "Micronesia"), + ("federation of malaysia", "Malaysia"), + ("federation of saint christopher and nevis", "St Kitts and Nevis"), + ("federationofmalaysia", "Malaysia"), + ("federationofsaintchristopherandnevis", "St Kitts and Nevis"), + ("federative republic of brazil", "Brazil"), + ("federativerepublicofbrazil", "Brazil"), + ("fi", "Finland"), + ("fj", "Fiji"), + ("fk", "Falkland Islands"), + ("fm", "Micronesia"), + ("fo", "Faroe Islands"), + ("fr", "France"), + ("french congo", "Congo"), + ("french guinea", "Guinea"), + ("french oceania", "French Polynesia"), + ("french republic", "France"), + ("french sudan", "Mali"), + ("frenchcongo", "Congo"), + ("frenchguiana", "French Guiana"), + ("frenchguinea", "Guinea"), + ("frenchoceania", "French Polynesia"), + ("frenchpolynesia", "French Polynesia"), + ("frenchrepublic", "France"), + ("frenchsouthernterritories", "French Southern Territories"), + ("frenchsudan", "Mali"), + ("frg", "Germany"), + ("friendly islands", "Tonga"), + ("friendlyislands", "Tonga"), + ("fÃĻrøerne", "Faroe Islands"), + ("føroyar", "Faroe Islands"), + ("ga", "Gabon"), + ("gabonese republic", "Gabon"), + ("gaboneserepublic", "Gabon"), + ("gabun", "Gabon"), + ("gabuuti", "Djibouti"), + ("gd", "Grenada"), + ("ge", "Georgia"), + ("genus argentina", "Argentina"), + ("genusargentina", "Argentina"), + ("germany democratic republic", "Germany"), + ("germanydemocraticrepublic", "Germany"), + ("gf", "French Guiana"), + ("gg", "United Kingdom"), + ("gh", "Ghana"), + ("gi", "Gibraltar"), + ("gine", "Guinea"), + ("gl", "Greenland"), + ("gm", "The Gambia"), + ("gn", "Guinea"), + ("gold coast", "Ghana"), + ("goldcoast", "Ghana"), + ("gp", "Guadeloupe"), + ("gq", "Equatorial Guinea"), + ("gr", "Greece"), + ("grand duchy of luxembourg", "Luxembourg"), + ("grandduchyofluxembourg", "Luxembourg"), + ("gronland", "Greenland"), + ("grønland", "Greenland"), + ("gs", "South Georgia and South Sandwich Islands"), + ("gt", "Guatemala"), + ("gu", "Guam"), + ("guinea ecuatorial", "Equatorial Guinea"), + ("guineaecuatorial", "Equatorial Guinea"), + ("guinÊe", "Guinea"), + ("guyane", "French Guiana"), + ("guÃĨhÃĨn", "Guam"), + ("gw", "Guinea-Bissau"), + ("gy", "Guyana"), + ("hanguk", "South Korea"), + ("hashemite kingdom of jordan", "Jordan"), + ("hashemitekingdomofjordan", "Jordan"), + ("hayastan", "Armenia"), + ("hayastÃĄn", "Armenia"), + ("haïti", "Haiti"), + ("heardislandandmcdonaldislands", "Heard Island and McDonald Islands"), + ("hellas", "Greece"), + ("hellenic republic", "Greece"), + ("hellenicrepublic", "Greece"), + ("henderson", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ("heung gong", "Hong Kong"), + ("heunggong", "Hong Kong"), + ("hindustan", "India"), + ("hk", "Hong Kong"), + ("hm", "Heard Island and McDonald Islands"), + ("hn", "Honduras"), + ("holland", "Netherlands"), + ("holy see", "Vatican City"), + ("holysee", "Vatican City"), + ("hong kong special administrative region", "Hong Kong"), + ("hongkong", "Hong Kong"), + ("hongkongspecialadministrativeregion", "Hong Kong"), + ("howlandisland", "Howland Island"), + ("hr", "Croatia"), + ("hrvatska", "Croatia"), + ("ht", "Haiti"), + ("hu", "Hungary"), + ("id", "Indonesia"), + ("ie", "Ireland"), + ("il", "Israel"), + ("ilikwet", "Kuwait"), + ("im", "United Kingdom"), + ("in", "India"), + ("independent state of papua new guinea", "Papua New Guinea"), + ("independent state of samoa", "Samoa"), + ("independentstateofpapuanewguinea", "Papua New Guinea"), + ("independentstateofsamoa", "Samoa"), + ("iningizimu afrika", "South Africa"), + ("iningizimuafrika", "South Africa"), + ("io", "British Indian Ocean Territory"), + ("iot", "British Indian Ocean Territory"), + ("iq", "Iraq"), + ("ir", "Iran"), + ("irak", "Iraq"), + ("irelend", "Ireland"), + ("irish republic", "Ireland"), + ("irishrepublic", "Ireland"), + ("iritriya", "Eritrea"), + ("is", "Iceland"), + ("isewula afrika", "South Africa"), + ("isewulaafrika", "South Africa"), + ("islamic republic of afghanistan", "Afghanistan"), + ("islamic republic of gambia", "The Gambia"), + ("islamic republic of iran", "Iran"), + ("islamic republic of mauritania", "Mauritania"), + ("islamic republic of pakistan", "Pakistan"), + ("islamicrepublicofafghanistan", "Afghanistan"), + ("islamicrepublicofgambia", "The Gambia"), + ("islamicrepublicofiran", "Iran"), + ("islamicrepublicofmauritania", "Mauritania"), + ("islamicrepublicofpakistan", "Pakistan"), + ("island of guernsey", "United Kingdom"), + ("island of jersey", "United Kingdom"), + ("island", "Iceland"), + ("islandofguernsey", "United Kingdom"), + ("islandofjersey", "United Kingdom"), + ("isleofman", "United Kingdom"), + ("israĘŧiyl", "Israel"), + ("isreal", "Israel"), + ("it", "Italy"), + ("italia", "Italy"), + ("italian republic", "Italy"), + ("italianrepublic", "Italy"), + ("itlay", "Italy"), + ("ivorycoast", "Ivory Coast"), + ("jabuuti", "Djibouti"), + ("jamaca", "Jamaica"), + ("jamacia", "Jamaica"), + ("jamahiriya", "Libya"), + ("jarvisisland", "Jarvis Island"), + ("je", "United Kingdom"), + ("jm", "Jamaica"), + ("jo", "Jordan"), + ("johnstonatoll", "Johnston Atoll"), + ("jp", "Japan"), + ("juzur alqamar", "Comoros"), + ("juzuralqamar", "Comoros"), + ("jèrri", "Tuvalu"), + ("jÄĢbÅĢtÄĢ", "Djibouti"), + ("kalaallit nunaat", "Greenland"), + ("kalaallitnunaat", "Greenland"), + ("kampuchea", "Cambodia"), + ("kangwane", "Eswatini"), + ("katar", "Qatar"), + ("kazakh", "Kazakhstan"), + ("kazakhstÃĄn", "Kazakhstan"), + ("kazakstan", "Kazakhstan"), + ("ke", "Kenya"), + ("kg", "Kyrgyzstan"), + ("kh", "Cambodia"), + ("ki", "Kiribati"), + ("kingdom of bahrain", "Bahrain"), + ("kingdom of belgium", "Belgium"), + ("kingdom of bhutan", "Bhutan"), + ("kingdom of cambodia", "Cambodia"), + ("kingdom of denmark", "Denmark"), + ("kingdom of eswatini", "Eswatini"), + ("kingdom of lesotho", "Lesotho"), + ("kingdom of moroccoalmagrib", "Morocco"), + ("kingdom of norway", "Norway"), + ("kingdom of saudi arabia", "Saudi Arabia"), + ("kingdom of spain", "Spain"), + ("kingdom of swaziland", "Eswatini"), + ("kingdom of sweden", "Sweden"), + ("kingdom of thailand", "Thailand"), + ("kingdom of the netherlands", "Netherlands"), + ("kingdom of tonga", "Tonga"), + ("kingdomofbahrain", "Bahrain"), + ("kingdomofbelgium", "Belgium"), + ("kingdomofbhutan", "Bhutan"), + ("kingdomofcambodia", "Cambodia"), + ("kingdomofdenmark", "Denmark"), + ("kingdomofeswatini", "Eswatini"), + ("kingdomoflesotho", "Lesotho"), + ("kingdomofmoroccoalmagrib", "Morocco"), + ("kingdomofnorway", "Norway"), + ("kingdomofsaudiarabia", "Saudi Arabia"), + ("kingdomofspain", "Spain"), + ("kingdomofswaziland", "Eswatini"), + ("kingdomofsweden", "Sweden"), + ("kingdomofthailand", "Thailand"), + ("kingdomofthenetherlands", "Netherlands"), + ("kingdomoftonga", "Tonga"), + ("kingmanreef", "Kingman Reef"), + ("kirghizia", "Kyrgyzstan"), + ("kirghizstan", "Kyrgyzstan"), + ("kirgiz", "Kyrgyzstan"), + ("kirgizia", "Kyrgyzstan"), + ("kirgizija", "Kyrgyzstan"), + ("kirgizstan", "Kyrgyzstan"), + ("km", "Comoros"), + ("kn", "St Kitts and Nevis"), + ("komori", "Comoros"), + ("kosova", "Kosovo"), + ("koweit", "Kuwait"), + ("kp", "North Korea"), + ("kr", "South Korea"), + ("kw", "Kuwait"), + ("ky", "Cayman Islands"), + ("kypros", "Cyprus"), + ("kyrgyz republic", "Kyrgyzstan"), + ("kyrgyz republickirghiz", "Kyrgyzstan"), + ("kyrgyzrepublic", "Kyrgyzstan"), + ("kyrgyzrepublickirghiz", "Kyrgyzstan"), + ("kz", "Kazakhstan"), + ("kÃ˛rsou", "Curaçao"), + ("kÃļdÃļrÃļsÃĒse tÃŽ bÃĒafrÃŽka", "Central African Republic"), + ("kÃļdÃļrÃļsÃĒsetÃŽbÃĒafrÃŽka", "Central African Republic"), + ("kÃŊpros", "Cyprus"), + ("kÄąbrÄąs", "Cyprus"), + ("la", "Laos"), + ("lao", "Laos"), + ("las malvinas", "Falkland Islands"), + ("lasmalvinas", "Falkland Islands"), + ("latvija", "Latvia"), + ("latvijaz", "Latvia"), + ("lb", "Lebanon"), + ("lc", "St Lucia"), + ("lebanese republic", "Lebanon"), + ("lebaneserepublic", "Lebanon"), + ("li", "Liechtenstein"), + ("lietuva", "Lithuania"), + ("lk", "Sri Lanka"), + ("lr", "Liberia"), + ("ls", "Lesotho"), + ("lt", "Lithuania"), + ("lu", "Luxembourg"), + ("lubnān", "Lebanon"), + ("luxemborg", "Luxembourg"), + ("luxemburg", "Luxembourg"), + ("lv", "Latvia"), + ("ly", "Libya"), + ("lÃĢtzebuerg", "Luxembourg"), + ("lÄĢbiyā", "Libya"), + ("ma", "Morocco"), + ("macao special administrative region", "Macao"), + ("macaospecialadministrativeregion", "Macao"), + ("macedon", "North Macedonia"), + ("madagasikara", "Madagascar"), + ("magyarorszag", "Hungary"), + ("magyarorszÃĄg", "Hungary"), + ("mainland china", "China"), + ("mainlandchina", "China"), + ("makedonija", "North Macedonia"), + ("malagasy republic", "Madagascar"), + ("malagasyrepublic", "Madagascar"), + ("malyasi", "Malaysia"), + ("malÄ“ášŖiyā", "Malaysia"), + ("maroc", "Morocco"), + ("marruecos", "Morocco"), + ("marshallislands", "Marshall Islands"), + ("masr", "Egypt"), + ("maurice", "Mauritius"), + ("mauritanie", "Mauritania"), + ("mc", "Monaco"), + ("md", "Moldova"), + ("me", "Montenegro"), + ("mexcio", "Mexico"), + ("mexicanos", "Mexico"), + ("mexixo", "Mexico"), + ("mf", "Saint-Martin (French part)"), + ("mg", "Madagascar"), + ("mh", "Marshall Islands"), + ("midwayislands", "Midway Islands"), + ("misr", "Egypt"), + ("mk", "North Macedonia"), + ("ml", "Mali"), + ("mm", "Myanmar (Burma)"), + ("mn", "Mongolia"), + ("mo", "Macao"), + ("mocambique", "Mozambique"), + ("moldavia", "Moldova"), + ("mongol uls", "Mongolia"), + ("mongoluls", "Mongolia"), + ("mongÎŗol ulus", "Mongolia"), + ("mongÎŗolulus", "Mongolia"), + ("moris", "Mauritius"), + ("moçambique", "Mozambique"), + ("mp", "Northern Mariana Islands"), + ("mq", "Martinique"), + ("mr", "Mauritania"), + ("ms", "Montserrat"), + ("mt", "Malta"), + ("mu", "Mauritius"), + ("mueang thai", "Thailand"), + ("mueangthai", "Thailand"), + ("muritan", "Mauritania"), + ("muritaniya", "Mauritania"), + ("muscat and oman", "Oman"), + ("muscatandoman", "Oman"), + ("mv", "Maldives"), + ("mw", "Malawi"), + ("mx", "Mexico"), + ("my", "Malaysia"), + ("myanma", "Myanmar (Burma)"), + ("myanmar(burma)", "Myanmar (Burma)"), + ("mz", "Mozambique"), + ("mÊxico", "Mexico"), + ("mēxihco", "Mexico"), + ("mÅĢrÄĢtānyā", "Mauritania"), + ("mĮŽlÃĄixÄĢyà", "Malaysia"), + ("na", "Namibia"), + ("namhan", "South Korea"), + ("namibiÃĢ", "Namibia"), + ("naoero", "Nauru"), + ("navassaisland", "Navassa Island"), + ("naíjíríà", "Nigeria"), + ("nc", "New Caledonia"), + ("ne", "Niger"), + ("nederland", "Netherlands"), + ("nederlÃĸn", "Netherlands"), + ("nepāl", "Nepal"), + ("new hebrides", "Vanuatu"), + ("newcaledonia", "New Caledonia"), + ("newhebrides", "Vanuatu"), + ("newzealand", "New Zealand"), + ("nf", "Norfolk Island"), + ("ng", "Nigeria"), + ("ngwane", "Eswatini"), + ("nihon", "Japan"), + ("nijar", "Niger"), + ("nijeriya", "Nigeria"), + ("nippon", "Japan"), + ("niuē", "Niue"), + ("nl", "Netherlands"), + ("no", "Norway"), + ("noreg", "Norway"), + ("norfolkisland", "Norfolk Island"), + ("norge", "Norway"), + ("northern marianas", "Northern Mariana Islands"), + ("northern rhodesia", "Zambia"), + ("northernmarianaislands", "Northern Mariana Islands"), + ("northernmarianas", "Northern Mariana Islands"), + ("northernrhodesia", "Zambia"), + ("northkorea", "North Korea"), + ("northmacedonia", "North Macedonia"), + ("nouvellecalÊdonie", "New Caledonia"), + ("np", "Nepal"), + ("nr", "Nauru"), + ("nu", "Niue"), + ("nyasaland", "Malawi"), + ("nz", "New Zealand"), + ("occupiedpalestinianterritories", "Occupied Palestinian Territories"), + ("oesterreich", "Austria"), + ("om", "Oman"), + ("oriental republic of uruguay", "Uruguay"), + ("orientalrepublicofuruguay", "Uruguay"), + ("osterreich", "Austria"), + ("outer mongolia", "Mongolia"), + ("outermongolia", "Mongolia"), + ("o‘zbekiston", "Uzbekistan"), + ("o’zbekstan", "Uzbekistan"), + ("pa", "Panama"), + ("palmyraatoll", "Palmyra Atoll"), + ("panamÃĄ", "Panama"), + ("papua niugini", "Papua New Guinea"), + ("papuanewguinea", "Papua New Guinea"), + ("papuaniugini", "Papua New Guinea"), + ("paraguÃĄi", "Paraguay"), + ("pe", "Peru"), + ("pelew", "Palau"), + ("peoples republic", "China"), + ("peoplesrepublic", "China"), + ("persia", "Iran"), + ("perÃē", "Peru"), + ("pf", "French Polynesia"), + ("pg", "Papua New Guinea"), + ("ph", "Philippines"), + ("philippine islands", "Philippines"), + ("philippineislands", "Philippines"), + ("phillippine", "Philippines"), + ("pilipinas", "Philippines"), + ("pinas", "Philippines"), + ("piruw", "Peru"), + ("pitcairn", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ( + "pitcairn,henderson,ducieandoenoislands", + "Pitcairn, Henderson, Ducie and Oeno Islands", + ), + ("pk", "Pakistan"), + ("pl", "Poland"), + ("plurinational state of bolivia", "Bolivia"), + ("plurinationalstateofbolivia", "Bolivia"), + ("pm", "Saint Pierre and Miquelon"), + ("pn", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ("png", "Papua New Guinea"), + ("polska", "Poland"), + ("polynÊsie française", "French Polynesia"), + ("polynÊsiefrançaise", "French Polynesia"), + ("porto rico", "Puerto Rico"), + ("portorico", "Puerto Rico"), + ("portuguesa", "Portugal"), + ("portuguese guinea", "Guinea-Bissau"), + ("portuguese republic", "Portugal"), + ("portugueseguinea", "Guinea-Bissau"), + ("portugueserepublic", "Portugal"), + ("pr", "Puerto Rico"), + ("prathet thai", "Thailand"), + ("prathetthai", "Thailand"), + ("prc", "China"), + ("principality of andorra", "Andorra"), + ("principality of liechtenstein", "Liechtenstein"), + ("principality of monaco", "Monaco"), + ("principalityofandorra", "Andorra"), + ("principalityofliechtenstein", "Liechtenstein"), + ("principalityofmonaco", "Monaco"), + ("prk", "North Korea"), + ("ps", "Occupied Palestinian Territories"), + ("pt", "Portugal"), + ("puarto rico", "Puerto Rico"), + ("puartorico", "Puerto Rico"), + ("puertorico", "Puerto Rico"), + ("pw", "Palau"), + ("py", "Paraguay"), + ("qa", "Qatar"), + ("qazaqstan", "Kazakhstan"), + ("rasalkhaimah", "Ras al-Khaimah"), + ("rastafari", "Jamaica"), + ("rastas", "Jamaica"), + ("ratchaanachak thai", "Thailand"), + ("ratchaanachakthai", "Thailand"), + ("re", "RÊunion"), + ("rep of ireland", "Ireland"), + ("repubblica", "San Marino"), + ("repubilika ya kongo", "Congo (Democratic Republic)"), + ("repubilikayakongo", "Congo (Democratic Republic)"), + ("republic of albania", "Albania"), + ("republic of angola", "Angola"), + ("republic of armenia", "Armenia"), + ("republic of austria", "Austria"), + ("republic of azerbaijan", "Azerbaijan"), + ("republic of belarusbelorussia", "Belarus"), + ("republic of benin", "Benin"), + ("republic of bosnia and herzegovina", "Bosnia and Herzegovina"), + ("republic of botswana", "Botswana"), + ("republic of bulgaria", "Bulgaria"), + ("republic of burundi", "Burundi"), + ("republic of cabo verde", "Cape Verde"), + ("republic of cameroon", "Cameroon"), + ("republic of chad", "Chad"), + ("republic of chile", "Chile"), + ("republic of colombia", "Colombia"), + ("republic of costa rica", "Costa Rica"), + ("republic of croatia", "Croatia"), + ("republic of cuba", "Cuba"), + ("republic of cyprus", "Cyprus"), + ("republic of djibouti", "Djibouti"), + ("republic of ecuador", "Ecuador"), + ("republic of el salvador", "El Salvador"), + ("republic of equatorial guinea", "Equatorial Guinea"), + ("republic of estonia", "Estonia"), + ("republic of fiji", "Fiji"), + ("republic of finland", "Finland"), + ("republic of ghana", "Ghana"), + ("republic of guatemala", "Guatemala"), + ("republic of guinea", "Guinea"), + ("republic of guineabissau", "Guinea-Bissau"), + ("republic of haiti", "Haiti"), + ("republic of honduras", "Honduras"), + ("republic of iceland", "Iceland"), + ("republic of india", "India"), + ("republic of indonesia", "Indonesia"), + ("republic of iraq", "Iraq"), + ("republic of ireland", "Ireland"), + ("republic of kazakhstankazak", "Kazakhstan"), + ("republic of kenya", "Kenya"), + ("republic of kiribati", "Kiribati"), + ("republic of korea", "South Korea"), + ("republic of kosovo", "Kosovo"), + ("republic of latvia", "Latvia"), + ("republic of liberia", "Liberia"), + ("republic of lithuanialietuva", "Lithuania"), + ("republic of macedonia", "North Macedonia"), + ("republic of madagascar", "Madagascar"), + ("republic of malawi", "Malawi"), + ("republic of maldives", "Maldives"), + ("republic of mali", "Mali"), + ("republic of malta", "Malta"), + ("republic of mauritius", "Mauritius"), + ("republic of moldova", "Moldova"), + ("republic of mozambique", "Mozambique"), + ("republic of namibia", "Namibia"), + ("republic of nauru", "Nauru"), + ("republic of nicaragua", "Nicaragua"), + ("republic of niger", "Niger"), + ("republic of palau", "Palau"), + ("republic of panama", "Panama"), + ("republic of paraguay", "Paraguay"), + ("republic of peru", "Peru"), + ("republic of poland", "Poland"), + ("republic of rwandaruanda", "Rwanda"), + ("republic of san marino", "San Marino"), + ("republic of senegal", "Senegal"), + ("republic of serbia", "Serbia"), + ("republic of seychelles", "Seychelles"), + ("republic of sierra leone", "Sierra Leone"), + ("republic of singapore", "Singapore"), + ("republic of slovenia", "Slovenia"), + ("republic of south africa", "South Africa"), + ("republic of south sudan", "South Sudan"), + ("republic of suriname", "Suriname"), + ("republic of tajikistantadjik", "Tajikistan"), + ("republic of the congo", "Congo"), + ("republic of the marshall islands", "Marshall Islands"), + ("republic of the philippines", "Philippines"), + ("republic of the sudan", "Sudan"), + ("republic of the union of myanmar", "Myanmar (Burma)"), + ("republic of trinidad and tobago", "Trinidad and Tobago"), + ("republic of turkey", "Turkey"), + ("republic of uganda", "Uganda"), + ("republic of uzbekistan", "Uzbekistan"), + ("republic of vanuatu", "Vanuatu"), + ("republic of yemen", "Yemen"), + ("republic of zambia", "Zambia"), + ("republic of zimbabwe", "Zimbabwe"), + ("republicofalbania", "Albania"), + ("republicofangola", "Angola"), + ("republicofarmenia", "Armenia"), + ("republicofaustria", "Austria"), + ("republicofazerbaijan", "Azerbaijan"), + ("republicofbelarusbelorussia", "Belarus"), + ("republicofbenin", "Benin"), + ("republicofbosniaandherzegovina", "Bosnia and Herzegovina"), + ("republicofbotswana", "Botswana"), + ("republicofbulgaria", "Bulgaria"), + ("republicofburundi", "Burundi"), + ("republicofcaboverde", "Cape Verde"), + ("republicofcameroon", "Cameroon"), + ("republicofchad", "Chad"), + ("republicofchile", "Chile"), + ("republicofcolombia", "Colombia"), + ("republicofcostarica", "Costa Rica"), + ("republicofcroatia", "Croatia"), + ("republicofcuba", "Cuba"), + ("republicofcyprus", "Cyprus"), + ("republicofdjibouti", "Djibouti"), + ("republicofecuador", "Ecuador"), + ("republicofelsalvador", "El Salvador"), + ("republicofequatorialguinea", "Equatorial Guinea"), + ("republicofestonia", "Estonia"), + ("republicoffiji", "Fiji"), + ("republicoffinland", "Finland"), + ("republicofghana", "Ghana"), + ("republicofguatemala", "Guatemala"), + ("republicofguinea", "Guinea"), + ("republicofguineabissau", "Guinea-Bissau"), + ("republicofhaiti", "Haiti"), + ("republicofhonduras", "Honduras"), + ("republicoficeland", "Iceland"), + ("republicofindia", "India"), + ("republicofindonesia", "Indonesia"), + ("republicofiraq", "Iraq"), + ("republicofireland", "Ireland"), + ("republicofkazakhstankazak", "Kazakhstan"), + ("republicofkenya", "Kenya"), + ("republicofkiribati", "Kiribati"), + ("republicofkorea", "South Korea"), + ("republicofkosovo", "Kosovo"), + ("republicoflatvia", "Latvia"), + ("republicofliberia", "Liberia"), + ("republicoflithuanialietuva", "Lithuania"), + ("republicofmacedonia", "North Macedonia"), + ("republicofmadagascar", "Madagascar"), + ("republicofmalawi", "Malawi"), + ("republicofmaldives", "Maldives"), + ("republicofmali", "Mali"), + ("republicofmalta", "Malta"), + ("republicofmauritius", "Mauritius"), + ("republicofmoldova", "Moldova"), + ("republicofmozambique", "Mozambique"), + ("republicofnamibia", "Namibia"), + ("republicofnauru", "Nauru"), + ("republicofnicaragua", "Nicaragua"), + ("republicofniger", "Niger"), + ("republicofpalau", "Palau"), + ("republicofpanama", "Panama"), + ("republicofparaguay", "Paraguay"), + ("republicofperu", "Peru"), + ("republicofpoland", "Poland"), + ("republicofrwandaruanda", "Rwanda"), + ("republicofsanmarino", "San Marino"), + ("republicofsenegal", "Senegal"), + ("republicofserbia", "Serbia"), + ("republicofseychelles", "Seychelles"), + ("republicofsierraleone", "Sierra Leone"), + ("republicofsingapore", "Singapore"), + ("republicofslovenia", "Slovenia"), + ("republicofsouthafrica", "South Africa"), + ("republicofsouthsudan", "South Sudan"), + ("republicofsuriname", "Suriname"), + ("republicoftajikistantadjik", "Tajikistan"), + ("republicofthecongo", "Congo"), + ("republicofthemarshallislands", "Marshall Islands"), + ("republicofthephilippines", "Philippines"), + ("republicofthesudan", "Sudan"), + ("republicoftheunionofmyanmar", "Myanmar (Burma)"), + ("republicoftrinidadandtobago", "Trinidad and Tobago"), + ("republicofturkey", "Turkey"), + ("republicofuganda", "Uganda"), + ("republicofuzbekistan", "Uzbekistan"), + ("republicofvanuatu", "Vanuatu"), + ("republicofyemen", "Yemen"), + ("republicofzambia", "Zambia"), + ("republicofzimbabwe", "Zimbabwe"), + ("repÃēblica dominicana", "Dominican Republic"), + ("repÃēblica oriental del uruguay", "Uruguay"), + ("repÃēblicadominicana", "Dominican Republic"), + ("repÃēblicaorientaldeluruguay", "Uruguay"), + ("reunion", "RÊunion"), + ("rgypt", "Egypt"), + ("rhodesia", "Zimbabwe"), + ("ro", "Romania"), + ("roi", "Ireland"), + ("romÃĸnia", "Romania"), + ("rossiya", "Russia"), + ("rossiÃĸ", "Russia"), + ("roumania", "Romania"), + ("rs", "Serbia"), + ("rsa", "South Africa"), + ("rsm", "San Marino"), + ("ru", "Russia"), + ("rumania", "Romania"), + ("russian federation", "Russia"), + ("russianfederation", "Russia"), + ("rw", "Rwanda"), + ("rwandese republic", "Rwanda"), + ("rwandeserepublic", "Rwanda"), + ("rÊpublique centrafricaine", "Central African Republic"), + ("rÊpublique du congo", "Congo"), + ("rÊpublique dÊmocratique du congo", "Congo (Democratic Republic)"), + ("rÊpublique française", "France"), + ("rÊpublique gabonaise", "Gabon"), + ("rÊpubliquecentrafricaine", "Central African Republic"), + ("rÊpubliqueducongo", "Congo"), + ("rÊpubliquedÊmocratiqueducongo", "Congo (Democratic Republic)"), + ("rÊpubliquefrançaise", "France"), + ("rÊpubliquegabonaise", "Gabon"), + ("sa", "Saudi Arabia"), + ("saint lucia", "St Lucia"), + ("saint vincent and the grenadines", "St Vincent"), + ("saintbarthÊlemy", "Saint BarthÊlemy"), + ("sainthelena", "Saint Helena"), + ("saintlucia", "St Lucia"), + ("saintmartin(frenchpart)", "Saint-Martin (French part)"), + ("saintpierre et miquelon", "Saint Pierre and Miquelon"), + ("saintpierreandmiquelon", "Saint Pierre and Miquelon"), + ("saintpierreetmiquelon", "Saint Pierre and Miquelon"), + ("saintvincentandthegrenadines", "St Vincent"), + ("sakartvelo", "Georgia"), + ("salvador", "El Salvador"), + ("samo", "Samoa"), + ("samoa i sisifo", "Samoa"), + ("samoaisisifo", "Samoa"), + ("sanmarino", "San Marino"), + ("sao thome e principe", "Sao Tome and Principe"), + ("sao tome e principe", "Sao Tome and Principe"), + ("saothomeeprincipe", "Sao Tome and Principe"), + ("saotomeandprincipe", "Sao Tome and Principe"), + ("saotomeeprincipe", "Sao Tome and Principe"), + ("sarnam sranangron", "Suriname"), + ("sarnam", "Suriname"), + ("sarnamsranangron", "Suriname"), + ("saudiarabia", "Saudi Arabia"), + ("sb", "Solomon Islands"), + ("sc", "Seychelles"), + ("schweiz", "Switzerland"), + ("sd", "Sudan"), + ("se", "Sweden"), + ("sesel", "Seychelles"), + ("sg", "Singapore"), + ("shac", "Ascension"), + ("shhl", "Saint Helena"), + ("shqipÃĢria", "Albania"), + ("shta", "Tristan da Cunha"), + ("si", "Slovenia"), + ("siam", "Thailand"), + ("sierraleone", "Sierra Leone"), + ("singapur", "Singapore"), + ("singapura", "Singapore"), + ("sint maarten", "Sint Maarten (Dutch part)"), + ("sinteustatius", "Sint Eustatius"), + ("sintmaarten", "Sint Maarten (Dutch part)"), + ("sintmaarten(dutchpart)", "Sint Maarten (Dutch part)"), + ("sion", "Israel"), + ("sj", "Svalbard and Jan Mayen"), + ("sk", "Slovakia"), + ("sl", "Sierra Leone"), + ("slovak republic", "Slovakia"), + ("slovakrepublic", "Slovakia"), + ("slovenija", "Slovenia"), + ("slovensko", "Slovakia"), + ("slovenskÃĄ", "Slovakia"), + ("sm", "San Marino"), + ("sn", "Senegal"), + ("so", "Somalia"), + ("socialist republic of vietnam", "Vietnam"), + ("socialistrepublicofvietnam", "Vietnam"), + ("solomon aelan", "Solomon Islands"), + ("solomonaelan", "Solomon Islands"), + ("solomonislands", "Solomon Islands"), + ("solomons", "Solomon Islands"), + ("soomaaliya", "Somalia"), + ("soudan", "Sudan"), + ( + "south georgia and the south sandwich islands", + "South Georgia and South Sandwich Islands", + ), + ("south west africa", "Namibia"), + ("southafrica", "South Africa"), + ("southgeorgiaandsouthsandwichislands", "South Georgia and South Sandwich Islands"), + ( + "southgeorgiaandthesouthsandwichislands", + "South Georgia and South Sandwich Islands", + ), + ("southkorea", "South Korea"), + ("southsudan", "South Sudan"), + ("southwestafrica", "Namibia"), + ("sovereign base areas of akrotiri and dhekelia", "Dhekelia"), + ("sovereignbaseareasofakrotirianddhekelia", "Dhekelia"), + ("spanish guinea", "Equatorial Guinea"), + ("spanishguinea", "Equatorial Guinea"), + ("sr", "Suriname"), + ("sranangron", "Suriname"), + ("srbija", "Serbia"), + ("sri lankā", "Sri Lanka"), + ("srilanka", "Sri Lanka"), + ("srilankā", "Sri Lanka"), + ("ss", "South Sudan"), + ("st barth", "Saint BarthÊlemy"), + ("st barthelemy", "Saint BarthÊlemy"), + ("st thomas and principe", "Sao Tome and Principe"), + ("st", "Sao Tome and Principe"), + ("state of bahrain", "Bahrain"), + ("state of eritrea", "Eritrea"), + ("state of israel", "Israel"), + ("state of kuwait", "Kuwait"), + ("state of qatar", "Qatar"), + ("stateofbahrain", "Bahrain"), + ("stateoferitrea", "Eritrea"), + ("stateofisrael", "Israel"), + ("stateofkuwait", "Kuwait"), + ("stateofqatar", "Qatar"), + ("stbarth", "Saint BarthÊlemy"), + ("stbarthelemy", "Saint BarthÊlemy"), + ("stkittsandnevis", "St Kitts and Nevis"), + ("stlucia", "St Lucia"), + ("stthomasandprincipe", "Sao Tome and Principe"), + ("stvincent", "St Vincent"), + ("suidafrika", "South Africa"), + ("suisse", "Switzerland"), + ("sultanate of oman", "Oman"), + ("sultanateofoman", "Oman"), + ("suomi", "Finland"), + ("suriyah", "Syria"), + ("sv", "El Salvador"), + ("svalbardandjanmayen", "Svalbard and Jan Mayen"), + ("sverige", "Sweden"), + ("svizra", "Switzerland"), + ("svizzera", "Switzerland"), + ("swatini", "Eswatini"), + ("swiss confederation", "Switzerland"), + ("swissconfederation", "Switzerland"), + ("switerland", "Switzerland"), + ("sx", "Sint Maarten (Dutch part)"), + ("sy", "Syria"), + ("syrian arab republic", "Syria"), + ("syrianarabrepublic", "Syria"), + ("sz", "Eswatini"), + ("sÃŖo tomÊ e príncipe", "Sao Tome and Principe"), + ("sÃŖotomÊepríncipe", "Sao Tome and Principe"), + ("sÊnÊgal", "Senegal"), + ("tadzhik", "Tajikistan"), + ("tadzhikistan", "Tajikistan"), + ("tajik", "Tajikistan"), + ("tc", "Turks and Caicos Islands"), + ("tchad", "Chad"), + ("td", "Chad"), + ("territory of american samoa", "American Samoa"), + ("territory of christmas island", "Christmas Island"), + ("territory of guam", "Guam"), + ( + "territory of heard island and mcdonald islands", + "Heard Island and McDonald Islands", + ), + ("territory of norfolk island", "Norfolk Island"), + ("territory of the cocos (keeling) islands", "Cocos (Keeling) Islands"), + ("territory of the wallis and futuna islands", "Wallis and Futuna"), + ("territoryofamericansamoa", "American Samoa"), + ("territoryofchristmasisland", "Christmas Island"), + ("territoryofguam", "Guam"), + ("territoryofheardislandandmcdonaldislands", "Heard Island and McDonald Islands"), + ("territoryofnorfolkisland", "Norfolk Island"), + ("territoryofthecocos(keeling)islands", "Cocos (Keeling) Islands"), + ("territoryofthewallisandfutunaislands", "Wallis and Futuna"), + ("tf", "French Southern Territories"), + ("tg", "Togo"), + ("th", "Thailand"), + ("the arab republic of egypt", "Egypt"), + ("the argentine republic", "Argentina"), + ("the bolivarian republic of venezuela", "Venezuela"), + ("the british indian ocean territory", "British Indian Ocean Territory"), + ("the central african republic", "Central African Republic"), + ("the commonwealth of australia", "Australia"), + ("the commonwealth of dominica", "Dominica"), + ("the commonwealth of the bahamas", "The Bahamas"), + ("the cooperative republic of guyana", "Guyana"), + ("the czech republic", "Czechia"), + ("the democratic republic of sao tome and principe", "Sao Tome and Principe"), + ("the democratic republic of the congo", "Congo (Democratic Republic)"), + ("the democratic republic of timorleste", "East Timor"), + ("the democratic socialist republic of sri lanka", "Sri Lanka"), + ("the dominican republic", "Dominican Republic"), + ("the federal democratic republic of ethiopia", "Ethiopia"), + ("the federal democratic republic of nepal", "Nepal"), + ("the federal republic of germany", "Germany"), + ("the federal republic of nigeria", "Nigeria"), + ("the federated states of micronesia", "Micronesia"), + ("the federation of saint christopher and nevis", "St Kitts and Nevis"), + ("the federative republic of brazil", "Brazil"), + ("the french republic", "France"), + ("the gabonese republic", "Gabon"), + ("the grand duchy of luxembourg", "Luxembourg"), + ("the hashemite kingdom of jordan", "Jordan"), + ("the hellenic republic", "Greece"), + ("the independent state of papua new guinea", "Papua New Guinea"), + ("the independent state of samoa", "Samoa"), + ("the islamic republic of afghanistan", "Afghanistan"), + ("the islamic republic of iran", "Iran"), + ("the islamic republic of mauritania", "Mauritania"), + ("the islamic republic of pakistan", "Pakistan"), + ("the italian republic", "Italy"), + ("the kingdom of bahrain", "Bahrain"), + ("the kingdom of belgium", "Belgium"), + ("the kingdom of bhutan", "Bhutan"), + ("the kingdom of cambodia", "Cambodia"), + ("the kingdom of denmark", "Denmark"), + ("the kingdom of lesotho", "Lesotho"), + ("the kingdom of morocco", "Morocco"), + ("the kingdom of norway", "Norway"), + ("the kingdom of saudi arabia", "Saudi Arabia"), + ("the kingdom of spain", "Spain"), + ("the kingdom of swaziland", "Eswatini"), + ("the kingdom of sweden", "Sweden"), + ("the kingdom of thailand", "Thailand"), + ("the kingdom of the netherlands", "Netherlands"), + ("the kingdom of tonga", "Tonga"), + ("the kyrgyz republic", "Kyrgyzstan"), + ("the lebanese republic", "Lebanon"), + ("the occupied palestinian territories", "Occupied Palestinian Territories"), + ("the oriental republic of uruguay", "Uruguay"), + ("the plurinational state of bolivia", "Bolivia"), + ("the portuguese republic", "Portugal"), + ("the principality of andorra", "Andorra"), + ("the principality of liechtenstein", "Liechtenstein"), + ("the principality of monaco", "Monaco"), + ("the republic of albania", "Albania"), + ("the republic of angola", "Angola"), + ("the republic of armenia", "Armenia"), + ("the republic of austria", "Austria"), + ("the republic of azerbaijan", "Azerbaijan"), + ("the republic of belarus", "Belarus"), + ("the republic of benin", "Benin"), + ("the republic of botswana", "Botswana"), + ("the republic of bulgaria", "Bulgaria"), + ("the republic of burundi", "Burundi"), + ("the republic of cabo verde", "Cape Verde"), + ("the republic of cameroon", "Cameroon"), + ("the republic of chad", "Chad"), + ("the republic of chile", "Chile"), + ("the republic of colombia", "Colombia"), + ("the republic of costa rica", "Costa Rica"), + ("the republic of croatia", "Croatia"), + ("the republic of cuba", "Cuba"), + ("the republic of cyprus", "Cyprus"), + ("the republic of djibouti", "Djibouti"), + ("the republic of ecuador", "Ecuador"), + ("the republic of el salvador", "El Salvador"), + ("the republic of equatorial guinea", "Equatorial Guinea"), + ("the republic of estonia", "Estonia"), + ("the republic of fiji", "Fiji"), + ("the republic of finland", "Finland"), + ("the republic of ghana", "Ghana"), + ("the republic of guatemala", "Guatemala"), + ("the republic of guinea", "Guinea"), + ("the republic of guineabissau", "Guinea-Bissau"), + ("the republic of haiti", "Haiti"), + ("the republic of honduras", "Honduras"), + ("the republic of iceland", "Iceland"), + ("the republic of india", "India"), + ("the republic of indonesia", "Indonesia"), + ("the republic of iraq", "Iraq"), + ("the republic of kazakhstan", "Kazakhstan"), + ("the republic of kenya", "Kenya"), + ("the republic of kiribati", "Kiribati"), + ("the republic of korea", "South Korea"), + ("the republic of kosovo", "Kosovo"), + ("the republic of latvia", "Latvia"), + ("the republic of liberia", "Liberia"), + ("the republic of lithuania", "Lithuania"), + ("the republic of macedonia", "North Macedonia"), + ("the republic of madagascar", "Madagascar"), + ("the republic of malawi", "Malawi"), + ("the republic of maldives", "Maldives"), + ("the republic of mali", "Mali"), + ("the republic of malta", "Malta"), + ("the republic of mauritius", "Mauritius"), + ("the republic of moldova", "Moldova"), + ("the republic of mozambique", "Mozambique"), + ("the republic of namibia", "Namibia"), + ("the republic of nauru", "Nauru"), + ("the republic of nicaragua", "Nicaragua"), + ("the republic of niger", "Niger"), + ("the republic of palau", "Palau"), + ("the republic of panama", "Panama"), + ("the republic of paraguay", "Paraguay"), + ("the republic of peru", "Peru"), + ("the republic of poland", "Poland"), + ("the republic of rwanda", "Rwanda"), + ("the republic of san marino", "San Marino"), + ("the republic of senegal", "Senegal"), + ("the republic of serbia", "Serbia"), + ("the republic of seychelles", "Seychelles"), + ("the republic of sierra leone", "Sierra Leone"), + ("the republic of singapore", "Singapore"), + ("the republic of slovenia", "Slovenia"), + ("the republic of south africa", "South Africa"), + ("the republic of south sudan", "South Sudan"), + ("the republic of suriname", "Suriname"), + ("the republic of tajikistan", "Tajikistan"), + ("the republic of the congo", "Congo"), + ("the republic of the gambia", "The Gambia"), + ("the republic of the marshall islands", "Marshall Islands"), + ("the republic of the philippines", "Philippines"), + ("the republic of the sudan", "Sudan"), + ("the republic of the union of myanmar", "Myanmar (Burma)"), + ("the republic of trinidad and tobago", "Trinidad and Tobago"), + ("the republic of turkey", "Turkey"), + ("the republic of uganda", "Uganda"), + ("the republic of uzbekistan", "Uzbekistan"), + ("the republic of vanuatu", "Vanuatu"), + ("the republic of yemen", "Yemen"), + ("the republic of zambia", "Zambia"), + ("the republic of zimbabwe", "Zimbabwe"), + ("the russian federation", "Russia"), + ("the slovak republic", "Slovakia"), + ("the socialist republic of vietnam", "Vietnam"), + ("the state of eritrea", "Eritrea"), + ("the state of israel", "Israel"), + ("the state of kuwait", "Kuwait"), + ("the state of qatar", "Qatar"), + ("the states", "United States"), + ("the sultanate of oman", "Oman"), + ("the swiss confederation", "Switzerland"), + ("the syrian arab republic", "Syria"), + ("the togolese republic", "Togo"), + ("the tunisian republic", "Tunisia"), + ("the union of the comoros", "Comoros"), + ("the united arab emirates", "United Arab Emirates"), + ("the united mexican states", "Mexico"), + ("the united republic of tanzania", "Tanzania"), + ("the united states of america", "United States"), + ("the virgin islands", "British Virgin Islands"), + ("thearabrepublicofegypt", "Egypt"), + ("theargentinerepublic", "Argentina"), + ("thebahamas", "The Bahamas"), + ("thebolivarianrepublicofvenezuela", "Venezuela"), + ("thebritishindianoceanterritory", "British Indian Ocean Territory"), + ("thecentralafricanrepublic", "Central African Republic"), + ("thecommonwealthofaustralia", "Australia"), + ("thecommonwealthofdominica", "Dominica"), + ("thecommonwealthofthebahamas", "The Bahamas"), + ("thecooperativerepublicofguyana", "Guyana"), + ("theczechrepublic", "Czechia"), + ("thedemocraticrepublicofsaotomeandprincipe", "Sao Tome and Principe"), + ("thedemocraticrepublicofthecongo", "Congo (Democratic Republic)"), + ("thedemocraticrepublicoftimorleste", "East Timor"), + ("thedemocraticsocialistrepublicofsrilanka", "Sri Lanka"), + ("thedominicanrepublic", "Dominican Republic"), + ("thefederaldemocraticrepublicofethiopia", "Ethiopia"), + ("thefederaldemocraticrepublicofnepal", "Nepal"), + ("thefederalrepublicofgermany", "Germany"), + ("thefederalrepublicofnigeria", "Nigeria"), + ("thefederatedstatesofmicronesia", "Micronesia"), + ("thefederationofsaintchristopherandnevis", "St Kitts and Nevis"), + ("thefederativerepublicofbrazil", "Brazil"), + ("thefrenchrepublic", "France"), + ("thegaboneserepublic", "Gabon"), + ("thegambia", "The Gambia"), + ("thegrandduchyofluxembourg", "Luxembourg"), + ("thehashemitekingdomofjordan", "Jordan"), + ("thehellenicrepublic", "Greece"), + ("theindependentstateofpapuanewguinea", "Papua New Guinea"), + ("theindependentstateofsamoa", "Samoa"), + ("theislamicrepublicofafghanistan", "Afghanistan"), + ("theislamicrepublicofiran", "Iran"), + ("theislamicrepublicofmauritania", "Mauritania"), + ("theislamicrepublicofpakistan", "Pakistan"), + ("theitalianrepublic", "Italy"), + ("thekingdomofbahrain", "Bahrain"), + ("thekingdomofbelgium", "Belgium"), + ("thekingdomofbhutan", "Bhutan"), + ("thekingdomofcambodia", "Cambodia"), + ("thekingdomofdenmark", "Denmark"), + ("thekingdomoflesotho", "Lesotho"), + ("thekingdomofmorocco", "Morocco"), + ("thekingdomofnorway", "Norway"), + ("thekingdomofsaudiarabia", "Saudi Arabia"), + ("thekingdomofspain", "Spain"), + ("thekingdomofswaziland", "Eswatini"), + ("thekingdomofsweden", "Sweden"), + ("thekingdomofthailand", "Thailand"), + ("thekingdomofthenetherlands", "Netherlands"), + ("thekingdomoftonga", "Tonga"), + ("thekyrgyzrepublic", "Kyrgyzstan"), + ("thelebaneserepublic", "Lebanon"), + ("theoccupiedpalestinianterritories", "Occupied Palestinian Territories"), + ("theorientalrepublicofuruguay", "Uruguay"), + ("theplurinationalstateofbolivia", "Bolivia"), + ("theportugueserepublic", "Portugal"), + ("theprincipalityofandorra", "Andorra"), + ("theprincipalityofliechtenstein", "Liechtenstein"), + ("theprincipalityofmonaco", "Monaco"), + ("therepublicofalbania", "Albania"), + ("therepublicofangola", "Angola"), + ("therepublicofarmenia", "Armenia"), + ("therepublicofaustria", "Austria"), + ("therepublicofazerbaijan", "Azerbaijan"), + ("therepublicofbelarus", "Belarus"), + ("therepublicofbenin", "Benin"), + ("therepublicofbotswana", "Botswana"), + ("therepublicofbulgaria", "Bulgaria"), + ("therepublicofburundi", "Burundi"), + ("therepublicofcaboverde", "Cape Verde"), + ("therepublicofcameroon", "Cameroon"), + ("therepublicofchad", "Chad"), + ("therepublicofchile", "Chile"), + ("therepublicofcolombia", "Colombia"), + ("therepublicofcostarica", "Costa Rica"), + ("therepublicofcroatia", "Croatia"), + ("therepublicofcuba", "Cuba"), + ("therepublicofcyprus", "Cyprus"), + ("therepublicofdjibouti", "Djibouti"), + ("therepublicofecuador", "Ecuador"), + ("therepublicofelsalvador", "El Salvador"), + ("therepublicofequatorialguinea", "Equatorial Guinea"), + ("therepublicofestonia", "Estonia"), + ("therepublicoffiji", "Fiji"), + ("therepublicoffinland", "Finland"), + ("therepublicofghana", "Ghana"), + ("therepublicofguatemala", "Guatemala"), + ("therepublicofguinea", "Guinea"), + ("therepublicofguineabissau", "Guinea-Bissau"), + ("therepublicofhaiti", "Haiti"), + ("therepublicofhonduras", "Honduras"), + ("therepublicoficeland", "Iceland"), + ("therepublicofindia", "India"), + ("therepublicofindonesia", "Indonesia"), + ("therepublicofiraq", "Iraq"), + ("therepublicofkazakhstan", "Kazakhstan"), + ("therepublicofkenya", "Kenya"), + ("therepublicofkiribati", "Kiribati"), + ("therepublicofkorea", "South Korea"), + ("therepublicofkosovo", "Kosovo"), + ("therepublicoflatvia", "Latvia"), + ("therepublicofliberia", "Liberia"), + ("therepublicoflithuania", "Lithuania"), + ("therepublicofmacedonia", "North Macedonia"), + ("therepublicofmadagascar", "Madagascar"), + ("therepublicofmalawi", "Malawi"), + ("therepublicofmaldives", "Maldives"), + ("therepublicofmali", "Mali"), + ("therepublicofmalta", "Malta"), + ("therepublicofmauritius", "Mauritius"), + ("therepublicofmoldova", "Moldova"), + ("therepublicofmozambique", "Mozambique"), + ("therepublicofnamibia", "Namibia"), + ("therepublicofnauru", "Nauru"), + ("therepublicofnicaragua", "Nicaragua"), + ("therepublicofniger", "Niger"), + ("therepublicofpalau", "Palau"), + ("therepublicofpanama", "Panama"), + ("therepublicofparaguay", "Paraguay"), + ("therepublicofperu", "Peru"), + ("therepublicofpoland", "Poland"), + ("therepublicofrwanda", "Rwanda"), + ("therepublicofsanmarino", "San Marino"), + ("therepublicofsenegal", "Senegal"), + ("therepublicofserbia", "Serbia"), + ("therepublicofseychelles", "Seychelles"), + ("therepublicofsierraleone", "Sierra Leone"), + ("therepublicofsingapore", "Singapore"), + ("therepublicofslovenia", "Slovenia"), + ("therepublicofsouthafrica", "South Africa"), + ("therepublicofsouthsudan", "South Sudan"), + ("therepublicofsuriname", "Suriname"), + ("therepublicoftajikistan", "Tajikistan"), + ("therepublicofthecongo", "Congo"), + ("therepublicofthegambia", "The Gambia"), + ("therepublicofthemarshallislands", "Marshall Islands"), + ("therepublicofthephilippines", "Philippines"), + ("therepublicofthesudan", "Sudan"), + ("therepublicoftheunionofmyanmar", "Myanmar (Burma)"), + ("therepublicoftrinidadandtobago", "Trinidad and Tobago"), + ("therepublicofturkey", "Turkey"), + ("therepublicofuganda", "Uganda"), + ("therepublicofuzbekistan", "Uzbekistan"), + ("therepublicofvanuatu", "Vanuatu"), + ("therepublicofyemen", "Yemen"), + ("therepublicofzambia", "Zambia"), + ("therepublicofzimbabwe", "Zimbabwe"), + ("therussianfederation", "Russia"), + ("theslovakrepublic", "Slovakia"), + ("thesocialistrepublicofvietnam", "Vietnam"), + ("thestateoferitrea", "Eritrea"), + ("thestateofisrael", "Israel"), + ("thestateofkuwait", "Kuwait"), + ("thestateofqatar", "Qatar"), + ("thestates", "United States"), + ("thesultanateofoman", "Oman"), + ("theswissconfederation", "Switzerland"), + ("thesyrianarabrepublic", "Syria"), + ("thetogoleserepublic", "Togo"), + ("thetunisianrepublic", "Tunisia"), + ("theunionofthecomoros", "Comoros"), + ("theunitedarabemirates", "United Arab Emirates"), + ("theunitedmexicanstates", "Mexico"), + ("theunitedrepublicoftanzania", "Tanzania"), + ("theunitedstatesofamerica", "United States"), + ("thevirginislands", "British Virgin Islands"), + ("timorleste", "East Timor"), + ("tj", "Tajikistan"), + ("tk", "Tokelau"), + ("tl", "East Timor"), + ("tm", "Turkmenistan"), + ("tn", "Tunisia"), + ("to", "Tonga"), + ("togolese republic", "Togo"), + ("togolese", "Togo"), + ("togoleserepublic", "Togo"), + ("tojikistan", "Tajikistan"), + ("toçikiston", "Tajikistan"), + ("tr", "Turkey"), + ("trinidadandtobago", "Trinidad and Tobago"), + ("tristandacunha", "Tristan da Cunha"), + ("tt", "Trinidad and Tobago"), + ("tunes", "Tunisia"), + ("tunisian republic", "Tunisia"), + ("tunisianrepublic", "Tunisia"), + ("turkiye", "Turkey"), + ("turkmen", "Turkmenistan"), + ("turkmenia", "Turkmenistan"), + ("turkomen", "Turkmenistan"), + ("turksandcaicosislands", "Turks and Caicos Islands"), + ("tv", "Tuvalu"), + ("tw", "Taiwan"), + ("tz", "Tanzania"), + ("tÃĄiwān", "Taiwan"), + ("tÃŧrkiye", "Turkey"), + ("tÃŧrkmenistan", "Turkmenistan"), + ("tÅĄÄd", "Chad"), + ("tÅĢns", "Tunisia"), + ("ua", "Ukraine"), + ("uae", "United Arab Emirates"), + ("ug", "Uganda"), + ("ukrayina", "Ukraine"), + ("ukraŅ—na", "Ukraine"), + ("umbuso weswatini", "Eswatini"), + ("umbusoweswatini", "Eswatini"), + ("ummalquwain", "Umm al-Quwain"), + ("umzantsi afrika", "South Africa"), + ("umzantsiafrika", "South Africa"), + ("union of the comoros", "Comoros"), + ("unionofthecomoros", "Comoros"), + ("unit states", "United States"), + ("unite states", "United States"), + ("united arab republic", "Egypt"), + ("united mexican states", "Mexico"), + ("united republic of tanzania", "Tanzania"), + ("united sat", "United States"), + ("united staes", "United States"), + ("united stated", "United States"), + ("united states america", "United States"), + ("united states of america", "United States"), + ("united stats", "United States"), + ("united sttes", "United States"), + ("unitedarabemirates", "United Arab Emirates"), + ("unitedarabrepublic", "Egypt"), + ("unitedmexicanstates", "Mexico"), + ("unitedrepublicoftanzania", "Tanzania"), + ("unitedsat", "United States"), + ("unitedstaes", "United States"), + ("unitedstated", "United States"), + ("unitedstates", "United States"), + ("unitedstatesofamerica", "United States"), + ("unitedstatesvirginislands", "United States Virgin Islands"), + ("unitedstats", "United States"), + ("unitedsttes", "United States"), + ("unites states", "United States"), + ("unitesstates", "United States"), + ("unitestates", "United States"), + ("unitstates", "United States"), + ("untied state", "United States"), + ("untiedstate", "United States"), + ("us", "United States"), + ("usa", "United States"), + ("uvea mo futuna", "Wallis and Futuna"), + ("uveamofutuna", "Wallis and Futuna"), + ("uy", "Uruguay"), + ("uz", "Uzbekistan"), + ("uzbek", "Uzbekistan"), + ("va", "Vatican City"), + ("vatican city state", "Vatican City"), + ("vaticancity", "Vatican City"), + ("vaticancitystate", "Vatican City"), + ("vc", "St Vincent"), + ("ve", "Venezuela"), + ("veitnam", "Vietnam"), + ("venezula", "Venezuela"), + ("vg", "British Virgin Islands"), + ("vi", "United States Virgin Islands"), + ("vietman", "Vietnam"), + ("virgin islands of the united states", "United States Virgin Islands"), + ("virgin islands", "British Virgin Islands"), + ("virgina islands", "British Virgin Islands"), + ("virginaislands", "British Virgin Islands"), + ("virginislands", "British Virgin Islands"), + ("virginislandsoftheunitedstates", "United States Virgin Islands"), + ("viti", "Fiji"), + ("viáģ‡t nam", "Vietnam"), + ("viáģ‡tnam", "Vietnam"), + ("vn", "Vietnam"), + ("volta", "Burkina Faso"), + ("volívia", "Bolivia"), + ("vu", "Vanuatu"), + ("wakeisland", "Wake Island"), + ("wallisandfutuna", "Wallis and Futuna"), + ("wallisetfutuna", "Wallis and Futuna"), + ("west pakistan", "Pakistan"), + ("western samoa", "Samoa"), + ("westernsahara", "Western Sahara"), + ("westernsamoa", "Samoa"), + ("westpakistan", "Pakistan"), + ("weswatini swatini ngwane", "Eswatini"), + ("weswatini", "Eswatini"), + ("weswatiniswatiningwane", "Eswatini"), + ("wf", "Wallis and Futuna"), + ("white russia", "Belarus"), + ("whiterussia", "Belarus"), + ("ws", "Samoa"), + ("wuliwya", "Bolivia"), + ("xk", "Kosovo"), + ("xqz", "Akrotiri"), + ("xxd", "Dhekelia"), + ("xÄĢnjiāpō", "Singapore"), + ("yaltopya", "Ethiopia"), + ("ye", "Yemen"), + ("yisrael", "Israel"), + ("yt", "Mayotte"), + ("za", "South Africa"), + ("zealnd", "New Zealand"), + ("zeland", "New Zealand"), + ("zhongguo", "China"), + ("zhonghua peoples republic", "China"), + ("zhonghua", "China"), + ("zhonghuapeoplesrepublic", "China"), + ("zhōngguÃŗ", "China"), + ("zhōnghuÃĄ mínguÃŗ", "Taiwan"), + ("zhōnghuÃĄmínguÃŗ", "Taiwan"), + ("zion", "Israel"), + ("zm", "Zambia"), + ("ztate of katar", "Qatar"), + ("ztateofkatar", "Qatar"), + ("zw", "Zimbabwe"), + ("ÃĨland", "Åland Islands"), + ("ÃĨlandislands", "Åland Islands"), + ("Êire", "Ireland"), + ("Êtatsunis", "United States"), + ("ísland", "Iceland"), + ("ÃŽraq", "Iraq"), + ("Ãļsterreich", "Austria"), + ("česko", "Czechia"), + ("českÃĄ republika", "Czechia"), + ("českÃĄ", "Czechia"), + ("českÃĄrepublika", "Czechia"), + ("ÄĢrān", "Iran"), + ("ÎĩÎģÎģÎŦδι", "Greece"), + ("ÎĩÎģÎģÎŦĪ‚", "Greece"), + ("ÎēĪĪ€ĪÎŋĪ‚", "Cyprus"), + ("ĐąĐĩĐģĐ°Ņ€ŅƒŅŅŒ", "Belarus"), + ("ĐąĐžŅĐŊа и Ņ…ĐĩҀ҆ĐĩĐŗĐžĐ˛Đ¸ĐŊа", "Bosnia and Herzegovina"), + ("ĐąĐžŅĐŊĐ°Đ¸Ņ…ĐĩҀ҆ĐĩĐŗĐžĐ˛Đ¸ĐŊа", "Bosnia and Herzegovina"), + ("ĐąŅŠĐģĐŗĐ°Ņ€Đ¸Ņ", "Bulgaria"), + ("ĐēĐ°ĐˇĐ°Ņ…ŅŅ‚Đ°ĐŊ", "Kazakhstan"), + ("ĐēĐ¸Ņ€ĐŗĐ¸ĐˇĐ¸Ņ", "Kyrgyzstan"), + ("ĐēĐžŅĐžĐ˛Đž", "Kosovo"), + ("ĐēŅ‹Ņ€ĐŗŅ‹ĐˇŅŅ‚Đ°ĐŊ", "Kyrgyzstan"), + ("ĐŧаĐēĐĩĐ´ĐžĐŊĐ¸Ņ˜Đ°", "North Macedonia"), + ("ĐŧĐžĐŊĐŗĐžĐģ ҃Đģҁ", "Mongolia"), + ("ĐŧĐžĐŊĐŗĐžĐģ҃Đģҁ", "Mongolia"), + ("Ņ€ĐžŅŅĐ¸ĐšŅĐēĐ°Ņ", "Russia"), + ("Ņ€ĐžŅŅĐ¸Ņ", "Russia"), + ("ŅŅ€ĐąĐ¸Ņ˜Đ° srbija", "Serbia"), + ("ŅŅ€ĐąĐ¸Ņ˜Đ°", "Serbia"), + ("ŅŅ€ĐąĐ¸Ņ˜Đ°srbija", "Serbia"), + ("Ņ‚ĐžŌˇĐ¸ĐēĐ¸ŅŅ‚ĐžĐŊ", "Tajikistan"), + ("҃ĐēŅ€Đ°Ņ—ĐŊа", "Ukraine"), + ("҆ҀĐŊа ĐŗĐžŅ€Đ°", "Montenegro"), + ("҆ҀĐŊĐ°ĐŗĐžŅ€Đ°", "Montenegro"), + ("ŅžĐˇĐąĐĩĐēĐ¸ŅŅ‚ĐžĐŊ", "Uzbekistan"), + ("Ō›Đ°ĐˇĐ°Ō›ŅŅ‚Đ°ĐŊ", "Kazakhstan"), + ("Õ°ÕĄÕĩÕĄÕŊÕŋÕĄÕļ", "Armenia"), + ("ישראל", "Israel"), + ("ØĨØąØĒØąŲŠØ§", "Eritrea"), + ("ØĨØŗØąØ§ØĻŲŠŲ„ ישראל", "Israel"), + ("ØĨØŗØąØ§ØĻŲŠŲ„", "Israel"), + ("ØĨØŗØąØ§ØĻŲŠŲ„×™×Š×¨××œ", "Israel"), + ("Ø§ŲØēØ§Ų†ØŗØĒØ§Ų†", "Afghanistan"), + ("Ø§Ų„ØŖØąØ¯Ų†", "Jordan"), + ("Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ Ø§Ų„ØšØąØ¨ŲŠŲ‘ØŠ Ø§Ų„Ų…ØĒŲ‘Ø­Ø¯ØŠ", "United Arab Emirates"), + ("Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒ", "United Arab Emirates"), + ("Ø§Ų„ØĨŲ…Ø§ØąØ§ØĒØ§Ų„ØšØąØ¨ŲŠŲ‘ØŠØ§Ų„Ų…ØĒŲ‘Ø­Ø¯ØŠ", "United Arab Emirates"), + ("Ø§Ų„Ø¨Ø­ØąŲŠŲ†", "Bahrain"), + ("Ø§Ų„ØŦØ˛Ø§ØĻØą", "Algeria"), + ("Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ", "Saudi Arabia"), + ("Ø§Ų„ØŗŲˆØ¯Ø§Ų†", "Sudan"), + ("Ø§Ų„ØĩŲˆŲ…Ø§Ų„", "Somalia"), + ("Ø§Ų„ØšØąØ§Ų‚", "Iraq"), + ("Ø§Ų„ØšØąØ§Ų‚\u200e", "Iraq"), + ("Ø§Ų„ŲƒŲˆŲŠØĒ", "Kuwait"), + ("Ø§Ų„Ų…ØēØąØ¨", "Morocco"), + ("Ø§Ų„Ų…Ų…Ų„ŲƒØŠ Ø§Ų„ØšØąØ¨ŲŠØŠ Ø§Ų„ØŗØšŲˆØ¯ŲŠØŠ", "Saudi Arabia"), + ("Ø§Ų„Ų…Ų…Ų„ŲƒØŠØ§Ų„ØšØąØ¨ŲŠØŠØ§Ų„ØŗØšŲˆØ¯ŲŠØŠ", "Saudi Arabia"), + ("Ø§Ų„Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØŠ", "Mauritania"), + ("Ø§Ų„ŲŠŲ…Ų†", "Yemen"), + ("Ø§ÛŒØąØ§Ų†", "Iran"), + ("Ø¨ØąŲˆŲ†ŲŠ", "Brunei"), + ("ØĒشاد", "Chad"), + ("ØĒشاد\u200e", "Chad"), + ("ØĒŲˆŲ†Øŗ", "Tunisia"), + ("ØŦØ˛ Ø§Ų„Ų‚Ų…Øą", "Comoros"), + ("ØŦØ˛Ø§Ų„Ų‚Ų…Øą", "Comoros"), + ("ØŦØ˛Øą Ø§Ų„Ų‚Ų…Øą", "Comoros"), + ("ØŦØ˛ØąØ§Ų„Ų‚Ų…Øą", "Comoros"), + ("ØŦŲŠØ¨ŲˆØĒ؊", "Djibouti"), + ("ØŦŲŠØ¨ŲˆØĒ؊\u200e", "Djibouti"), + ("Ø¯ŲˆŲ„ØŠ Ø§Ų„ŲƒŲˆŲŠØĒ", "Kuwait"), + ("Ø¯ŲˆŲ„ØŠØ§Ų„ŲƒŲˆŲŠØĒ", "Kuwait"), + ("ØŗŲˆØąŲŠØŠ", "Syria"), + ("ØšŲ…Ø§Ų†", "Oman"), + ("ØšŲŲ…Ø§Ų†", "Oman"), + ("ŲŲ„ØŗØˇŲŠŲ†", "Occupied Palestinian Territories"), + ("Ų‚Ø§Ø˛Ø§Ų‚ØŗØĒØ§Ų†", "Kazakhstan"), + ("Ų‚ØˇØą", "Qatar"), + ("Ų„Ø¨Ų†Ø§Ų†", "Lebanon"), + ("Ų„ØĩØ­ØąØ§ØĄ Ø§Ų„ØēØąØ¨ŲŠØŠ", "Western Sahara"), + ("Ų„ØĩØ­ØąØ§ØĄØ§Ų„ØēØąØ¨ŲŠØŠ", "Western Sahara"), + ("Ų„ŲŠØ¨ŲŠØ§", "Libya"), + ("Ų…ØĩØą", "Egypt"), + ("Ų…ŲˆØąŲŠØĒØ§Ų†ŲŠØ§", "Mauritania"), + ("ŲžØ§ÚŠØŗØĒØ§Ų†", "Pakistan"), + ("⤍āĨ‡ā¤Ēā¤žā¤˛", "Nepal"), + ("ā¤Ģā¤ŧā¤ŋ⤜āĨ€", "Fiji"), + ("ā¤­ā¤žā¤°ā¤¤ ā¤—ā¤Ŗā¤°ā¤žā¤œāĨā¤¯", "India"), + ("ā¤­ā¤žā¤°ā¤¤", "India"), + ("ā¤­ā¤žā¤°ā¤¤ā¤—ā¤Ŗā¤°ā¤žā¤œāĨā¤¯", "India"), + ("ā¤­ā¤žā¤°ā¤¤ā¤ŽāĨ", "India"), + ("⤭āĨ‚ā¤Ÿā¤žā¤¨", "Bhutan"), + ("ā¤ļ⤰āĨā¤¨ā¤ŽāĨ", "Suriname"), + ("āĻŦāĻžāĻ‚āϞāĻžāĻĻ⧇āĻļ", "Bangladesh"), + ("āĻ­āĻžāϰāϤ", "India"), + ("āĻ­āĻžā§°āϤ", "India"), + ("ā¨­ā¨žā¨°ā¨¤", "India"), + ("āĒ­āĒžāǰāǤ", "India"), + ("āŦ­āŦžāŦ°āŦ¤", "India"), + ("āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ", "Sri Lanka"), + ("āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯ āŽ•ā¯āŽŸāŽŋāŽ¯āŽ°āŽšā¯", "Singapore"), + ("āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯", "Singapore"), + ("āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯āŽ•ā¯", "Singapore"), + ("āŽšāŽŋāŽ™ā¯āŽ•āŽĒā¯āŽĒā¯‚āŽ°ā¯āŽ•ā¯āŽŸāŽŋāŽ¯āŽ°āŽšā¯", "Singapore"), + ("āŽŸāŽŋāŽ¯āŽ°āŽšā¯", "Singapore"), + ("āŽĒāŽžāŽ°āŽ¤āŽŽā¯", "India"), + ("āŽŽāŽ˛ā¯‡āŽšāŽŋāŽ¯āŽž", "Malaysia"), + ("ā°­ā°žā°°ā°¤ ā°Ļāą‡ā°ļā°‚", "India"), + ("ā°­ā°žā°°ā°¤ā°Ļāą‡ā°ļā°‚", "India"), + ("ā˛­ā˛žā˛°ā˛¤", "India"), + ("ā´­ā´žā´°ā´¤ā´‚", "India"), + # ('⎁⎊āļģ⎓ āļŊāļ‚āļšāˇ āŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ', 'Sri Lanka'), + # ('⎁⎊āļģ⎓ āļŊāļ‚āļšāˇ', 'Sri Lanka'), + # ('⎁⎊āļģ⎓ āļŊāļ‚āļšāˇāˇ€', 'Sri Lanka'), + # ('⎁⎊āļģ⎓āļŊāļ‚āļšāˇ', 'Sri Lanka'), + # ('⎁⎊āļģ⎓āļŊāļ‚āļšāˇāŽ‡āŽ˛āŽ™ā¯āŽ•ā¯ˆ', 'Sri Lanka'), + # ('⎁⎊āļģ⎓āļŊāļ‚āļšāˇāˇ€', 'Sri Lanka'), + ("ā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨āš„ā¸—ā¸ĸ", "Thailand"), + ("ā¸Ŗā¸˛ā¸Šā¸­ā¸˛ā¸“ā¸˛ā¸ˆā¸ąā¸ā¸Ŗāš„ā¸—ā¸ĸ", "Thailand"), + ("āš€ā¸Ąā¸ˇā¸­ā¸‡āš„ā¸—ā¸ĸ", "Thailand"), + ("āē›āē°āģ€āē—āē”āēĨāē˛āē§", "Laos"), + ("āŊ āŊ–āž˛āŊ´āŊ‚āŧ‹āŊĄāŊ´āŊŖ", "Bhutan"), + ("မá€ŧနá€ēမá€Ŧ", "Myanmar (Burma)"), + ("ქაáƒĨართველო", "Georgia"), + ("áŠĸá‰ĩዮáŒĩá‹Ģ", "Ethiopia"), + ("ኤርá‰ĩáˆĢ", "Eritrea"), + ("កម្ពážģជážļ", "Cambodia"), + ("᠎ᠤ᠊ᠭᠤᠯ ᠤᠯᠤᠰ", "Mongolia"), + ("᠎ᠤ᠊ᠭᠤᠯᠤᠯᠤᠰ", "Mongolia"), + ("‘umān", "Oman"), + ("â´°â´ŗâ´°âĩĄâĩ›", "Mauritania"), + ("â´°âĩŽâĩ”âĩ”âĩ“â´Ŋ", "Morocco"), + ("â´ˇâĩŖâ´°âĩĸâ´ģâĩ”", "Algeria"), + ("âĩâĩ‰â´ąâĩĸâ´°", "Libya"), + ("âĩâĩŽâĩ–âĩ”âĩ‰â´ą", "Morocco"), + ("âĩŽâĩ“âĩ”âĩ‰âĩœâ´°âĩ", "Mauritania"), + ("âĩœâĩ“âĩâĩ™", "Tunisia"), + ("中华", "China"), + ("中å›Ŋ", "China"), + ("中å›Ŋ/中华", "China"), + ("ä¸­č¯æ°‘åœ‹", "Taiwan"), + ("å°įŖ", "Taiwan"), + ("æ–°åŠ åĄ", "Singapore"), + ("æ–°åŠ åĄå…ąå’Œå›Ŋ", "Singapore"), + ("æ—ĨæœŦ", "Japan"), + ("č‡ēၪ", "Taiwan"), + ("č‡ēၪ/å°įŖ", "Taiwan"), + ("éĻ™æ¸¯", "Hong Kong"), + ("éŠŦæĨčĨŋäēš", "Malaysia"), + ("남한", "South Korea"), + ("ëļėĄ°ė„ ", "North Korea"), + ("ėĄ°ė„  / 朝鎎", "North Korea"), + ("ėĄ°ė„ /朝鎎", "North Korea"), + ("한ęĩ­ / 韓國", "South Korea"), + ("한ęĩ­/韓國", "South Korea"), +) + + +CROWDSOURCED_MISTAKES = ( + ("burma", "Myanmar (Burma)", "rest-of-world"), + ("canary islands", "Canary Islands", "europe"), + ("czechoslovakia", "Czechia", "europe"), + ("east germany", "Germany", "europe"), + ("easter island", "Easter Island", "rest-of-world"), + ("Eidal", "Italy", "europe"), + ("falklands", "Falkland Islands", "rest-of-world"), + ("Ffrainc", "France", "europe"), + ("hawaii", "United States", "rest-of-world"), + ("Herm", "United Kingdom", "united-kingdom"), + ("Khazakhstan", "Kazakhstan", "europe"), + ("korea", "South Korea", "rest-of-world"), + ("macau", "Macao", "rest-of-world"), + ("myanmar", "Myanmar (Burma)", "rest-of-world"), + ("new zeeland", "New Zealand", "rest-of-world"), + ("NI", "United Kingdom", "united-kingdom"), + ("Pitcairn Island", "Pitcairn, Henderson, Ducie and Oeno Islands", "rest-of-world"), + ("republic of china", "Taiwan", "rest-of-world"), + ("Republik Österreich", "Austria", "europe"), + ("RÊpublique Islamique de Mauritanie", "Mauritania", "rest-of-world"), + ("Mauritanie", "Mauritania", "rest-of-world"), + ("Sark", "United Kingdom", "united-kingdom"), + ("South Ireland", "Ireland", "europe"), + ("St Helena", "Saint Helena", "rest-of-world"), + ("Swaziland", "Eswatini", "rest-of-world"), + ("the falkland islands", "Falkland Islands", "rest-of-world"), + ("the falklands", "Falkland Islands", "rest-of-world"), + ( + "the sandwich islands", + "South Georgia and the South Sandwich Islands", + "rest-of-world", + ), + ("South Georgia", "South Georgia and the South Sandwich Islands", "rest-of-world"), + ("Tristan", "Tristan da Cunha", "rest-of-world"), + ("ussr", None, None), + ("USSR", None, None), + ("ŅĐžŅŽĐˇ ŅĐžĐ˛Đĩ҂ҁĐēĐ¸Ņ… ŅĐžŅ†Đ¸Đ°ĐģĐ¸ŅŅ‚Đ¸Ņ‡ĐĩҁĐēĐ¸Ņ… Ņ€ĐĩҁĐŋŅƒĐąĐģиĐē", None, None), + ("Vatican", "Vatican City", "europe"), + ("West Germany", "Germany", "europe"), + ("Yr Alban", "United Kingdom", "united-kingdom"), + ("yugoslavia", None, None), + ("jugoslavija", None, None), + ("Swistir", "Switzerland", "europe"), + ("Y Swistir", "Switzerland", "europe"), + ("Saint Kitts", "St Kitts and Nevis", "rest-of-world"), + ("St Kitts", "St Kitts and Nevis", "rest-of-world"), +) diff --git a/tests/notification_utils/test_base64_uuid.py b/tests/notification_utils/test_base64_uuid.py new file mode 100644 index 000000000..e9d35b24d --- /dev/null +++ b/tests/notification_utils/test_base64_uuid.py @@ -0,0 +1,57 @@ +import binascii +import os +from uuid import UUID + +import pytest + +from notifications_utils.base64_uuid import ( + base64_to_bytes, + base64_to_uuid, + bytes_to_base64, + uuid_to_base64, +) + + +def test_bytes_to_base64_to_bytes(): + b = os.urandom(32) + b64 = bytes_to_base64(b) + assert base64_to_bytes(b64) == b + + +@pytest.mark.parametrize( + "url_val", + [ + "AAAAAAAAAAAAAAAAAAAAAQ", + "AAAAAAAAAAAAAAAAAAAAAQ=", # even though this has invalid padding we put extra =s on the end so this is okay + "AAAAAAAAAAAAAAAAAAAAAQ==", + ], +) +def test_base64_converter_to_python(url_val): + assert base64_to_uuid(url_val) == UUID(int=1) + + +@pytest.mark.parametrize( + "python_val", [UUID(int=1), "00000000-0000-0000-0000-000000000001"] +) +def test_base64_converter_to_url(python_val): + assert uuid_to_base64(python_val) == "AAAAAAAAAAAAAAAAAAAAAQ" + + +@pytest.mark.parametrize( + "url_val,expectation", + [ + ( + "this_is_valid_base64_but_is_too_long_to_be_a_uuid", + pytest.raises(binascii.Error), + ), + ("this_one_has_emoji_➕➕➕", pytest.raises(UnicodeEncodeError)), + ], +) +def test_base64_converter_to_python_raises_validation_error(url_val, expectation): + with expectation: + base64_to_uuid(url_val) + + +def test_base64_converter_to_url_raises_validation_error(): + with pytest.raises(AttributeError): + uuid_to_base64(object()) diff --git a/tests/notification_utils/test_base_template.py b/tests/notification_utils/test_base_template.py new file mode 100644 index 000000000..c9d126d12 --- /dev/null +++ b/tests/notification_utils/test_base_template.py @@ -0,0 +1,119 @@ +from unittest.mock import patch + +import pytest + +from notifications_utils.template import SubjectMixin, Template + + +class ConcreteImplementation: + template_type = None + + # Can’t instantiate and test templates unless they implement __str__ + def __str__(self): + pass + + +class ConcreteTemplate(ConcreteImplementation, Template): + pass + + +class ConcreteTemplateWithSubject(SubjectMixin, ConcreteTemplate): + pass + + +def test_class(): + assert ( + repr(ConcreteTemplate({"content": "hello ((name))"})) + == 'ConcreteTemplate("hello ((name))", {})' + ) + + +def test_passes_through_template_attributes(): + assert ConcreteTemplate({"content": ""}).name is None + assert ( + ConcreteTemplate({"content": "", "name": "Two week reminder"}).name + == "Two week reminder" + ) + assert ConcreteTemplate({"content": ""}).id is None + assert ConcreteTemplate({"content": "", "id": "1234"}).id == "1234" + assert ConcreteTemplate({"content": ""}).template_type is None + + +def test_passes_through_subject(): + assert ( + ConcreteTemplateWithSubject( + {"content": "", "subject": "Your tax is due"} + ).subject + == "Your tax is due" + ) + + +def test_errors_for_missing_template_content(): + with pytest.raises(KeyError): + ConcreteTemplate({}) + + +@pytest.mark.parametrize("template", [0, 1, 2, True, False, None]) +def test_errors_for_invalid_template_types(template): + with pytest.raises(TypeError): + ConcreteTemplate(template) + + +@pytest.mark.parametrize("values", [[], False]) +def test_errors_for_invalid_values(values): + with pytest.raises(TypeError): + ConcreteTemplate({"content": ""}, values) + + +def test_matches_keys_to_placeholder_names(): + template = ConcreteTemplate({"content": "hello ((name))"}) + + template.values = {"NAME": "Chris"} + assert template.values == {"name": "Chris"} + + template.values = {"NAME": "Chris", "Town": "London"} + assert template.values == {"name": "Chris", "Town": "London"} + assert template.additional_data == {"Town"} + + template.values = None + assert template.missing_data == ["name"] + + +@pytest.mark.parametrize( + "template_content, template_subject, expected", + [ + ("the quick brown fox", "jumps", []), + ("the quick ((colour)) fox", "jumps", ["colour"]), + ("the quick ((colour)) ((animal))", "jumps", ["colour", "animal"]), + ("((colour)) ((animal)) ((colour)) ((animal))", "jumps", ["colour", "animal"]), + ("the quick brown fox", "((colour))", ["colour"]), + ("the quick ((colour)) ", "((animal))", ["animal", "colour"]), + ("((colour)) ((animal)) ", "((colour)) ((animal))", ["colour", "animal"]), + ("Dear ((name)), ((warning?? This is a warning))", "", ["name", "warning"]), + ("((warning? one question mark))", "", ["warning? one question mark"]), + ], +) +def test_extracting_placeholders(template_content, template_subject, expected): + assert ( + ConcreteTemplateWithSubject( + {"content": template_content, "subject": template_subject} + ).placeholders + == expected + ) + + +def test_random_variable_retrieve(): + template = ConcreteTemplate({"content": "content", "created_by": "now"}) + assert template.get_raw("created_by") == "now" + assert template.get_raw("missing", default="random") == "random" + assert template.get_raw("missing") is None + + +def test_compare_template(): + with patch( + "notifications_utils.template_change.TemplateChange.__init__", return_value=None + ) as mocked: + old_template = ConcreteTemplate({"content": "faked"}) + new_template = ConcreteTemplate({"content": "faked"}) + old_template.compare_to(new_template) + mocked.assert_called_once_with(old_template, new_template) diff --git a/tests/notification_utils/test_countries.py b/tests/notification_utils/test_countries.py new file mode 100644 index 000000000..473b2731b --- /dev/null +++ b/tests/notification_utils/test_countries.py @@ -0,0 +1,170 @@ +import pytest + +from notifications_utils.countries import Country, CountryMapping, CountryNotFoundError +from notifications_utils.countries.data import ( + _EUROPEAN_ISLANDS_LIST, + _UK_ISLANDS_LIST, + ADDITIONAL_SYNONYMS, + COUNTRIES_AND_TERRITORIES, + ROYAL_MAIL_EUROPEAN, + UK, + UK_ISLANDS, + WELSH_NAMES, + Postage, +) + +from .country_synonyms import ALL as ALL_SYNONYMS +from .country_synonyms import CROWDSOURCED_MISTAKES + + +def test_constants(): + assert UK == "United Kingdom" + assert UK_ISLANDS == [ + ("Alderney", UK), + ("Brecqhou", UK), + ("Guernsey", UK), + ("Herm", UK), + ("Isle of Man", UK), + ("Jersey", UK), + ("Jethou", UK), + ("Sark", UK), + ] + assert Postage.EUROPE == "europe" + assert Postage.REST_OF_WORLD == "rest-of-world" + assert Postage.UK == "united-kingdom" + + +@pytest.mark.parametrize("synonym, canonical", ADDITIONAL_SYNONYMS) +def test_hand_crafted_synonyms_map_to_canonical_countries(synonym, canonical): + exceptions_to_canonical_countries = [ + "Easter Island", + "South Georgia and the South Sandwich Islands", + ] + + synonyms = dict(COUNTRIES_AND_TERRITORIES).keys() + canonical_names = list(dict(COUNTRIES_AND_TERRITORIES).values()) + + assert canonical in ( + canonical_names + + _EUROPEAN_ISLANDS_LIST + + _UK_ISLANDS_LIST + + exceptions_to_canonical_countries + ) + + assert synonym not in {CountryMapping.make_key(synonym_) for synonym_ in synonyms} + assert Country(synonym).canonical_name == canonical + + +@pytest.mark.parametrize("welsh_name, canonical", WELSH_NAMES) +def test_welsh_names_map_to_canonical_countries(welsh_name, canonical): + assert Country(canonical).canonical_name == canonical + assert Country(welsh_name).canonical_name == canonical + + +def test_all_synonyms(): + for search, expected in ALL_SYNONYMS: + assert Country(search).canonical_name == expected + + +def test_crowdsourced_test_data(): + for search, expected_country, expected_postage in CROWDSOURCED_MISTAKES: + if expected_country or expected_postage: + assert Country(search).canonical_name == expected_country + assert Country(search).postage_zone == expected_postage + + +@pytest.mark.parametrize( + "search, expected", + ( + ("u.s.a", "United States"), + ("america", "United States"), + ("United States America", "United States"), + ("ROI", "Ireland"), + ("Irish Republic", "Ireland"), + ("Rep of Ireland", "Ireland"), + ("RepOfIreland", "Ireland"), + ("deutschland", "Germany"), + ("UK", "United Kingdom"), + ("England", "United Kingdom"), + ("Northern Ireland", "United Kingdom"), + ("Scotland", "United Kingdom"), + ("Wales", "United Kingdom"), + ("N. Ireland", "United Kingdom"), + ("GB", "United Kingdom"), + ("NIR", "United Kingdom"), + ("SCT", "United Kingdom"), + ("WLS", "United Kingdom"), + ("gambia", "The Gambia"), + ("Jersey", "United Kingdom"), + ("Guernsey", "United Kingdom"), + ("Lubnān", "Lebanon"), + ("Lubnan", "Lebanon"), + ("ESPAÑA", "Spain"), + ("ESPANA", "Spain"), + ("the democratic people's republic of korea", "North Korea"), + ("the democratic peoples republic of korea", "North Korea"), + ("ALAND", "Åland Islands"), + ("Sao Tome + Principe", "Sao Tome and Principe"), + ("Sao Tome & Principe", "Sao Tome and Principe"), + ("Antigua, and Barbuda", "Antigua and Barbuda"), + ("Azores", "Azores"), + ("Autonomous Region of the Azores", "Azores"), + ("Canary Islands", "Canary Islands"), + ("Islas Canarias", "Canary Islands"), + ("Canaries", "Canary Islands"), + ("Madeira", "Madeira"), + ("Autonomous Region of Madeira", "Madeira"), + ("RegiÃŖo AutÃŗnoma da Madeira", "Madeira"), + ("Balearic Islands", "Balearic Islands"), + ("Islas Baleares", "Balearic Islands"), + ("Illes Balears", "Balearic Islands"), + ("Corsica", "Corsica"), + ("Corse", "Corsica"), + ), +) +def test_hand_crafted_synonyms(search, expected): + assert Country(search).canonical_name == expected + + +def test_auto_checking_for_country_starting_with_the(): + canonical_names = dict(COUNTRIES_AND_TERRITORIES).values() + synonyms = dict(COUNTRIES_AND_TERRITORIES).keys() + assert "The Gambia" in canonical_names + assert "Gambia" not in synonyms + assert Country("Gambia").canonical_name == "The Gambia" + + +@pytest.mark.parametrize( + "search, expected_error_message", + ( + ("Qumran", "Not a known country or territory (Qumran)"), + ("Kumrahn", "Not a known country or territory (Kumrahn)"), + ), +) +def test_non_existant_countries(search, expected_error_message): + with pytest.raises(KeyError) as error: + Country(search) + assert str(error.value) == repr(expected_error_message) + assert isinstance(error.value, CountryNotFoundError) + + +@pytest.mark.parametrize( + "search, expected", + ( + ("u.s.a", "rest-of-world"), + ("Rep of Ireland", "europe"), + ("deutschland", "europe"), + ("UK", "united-kingdom"), + ("Jersey", "united-kingdom"), + ("Guernsey", "united-kingdom"), + ("isle-of-man", "united-kingdom"), + ("ESPAÑA", "europe"), + ), +) +def test_get_postage(search, expected): + assert Country(search).postage_zone == expected + + +def test_euro_postage_zone(): + for search in ROYAL_MAIL_EUROPEAN: + assert Country(search).postage_zone == Postage.EUROPE diff --git a/tests/notification_utils/test_countries_iso.py b/tests/notification_utils/test_countries_iso.py new file mode 100644 index 000000000..e1d3a68d8 --- /dev/null +++ b/tests/notification_utils/test_countries_iso.py @@ -0,0 +1,526 @@ +import pytest + +from notifications_utils.countries import Country, CountryNotFoundError + + +def _country_not_found(*test_case): + return pytest.param( + *test_case, + marks=pytest.mark.xfail(raises=CountryNotFoundError), + ) + + +@pytest.mark.parametrize( + "alpha_2, expected_name", + ( + ("AF", "Afghanistan"), + ("AL", "Albania"), + ("DZ", "Algeria"), + ("AS", "American Samoa"), + ("AD", "Andorra"), + ("AO", "Angola"), + ("AI", "Anguilla"), + ("AQ", "Antarctica"), + ("AG", "Antigua and Barbuda"), + ("AR", "Argentina"), + ("AM", "Armenia"), + ("AW", "Aruba"), + ("AU", "Australia"), + ("AT", "Austria"), + ("AZ", "Azerbaijan"), + ("BS", "The Bahamas"), + ("BH", "Bahrain"), + ("BD", "Bangladesh"), + ("BB", "Barbados"), + ("BY", "Belarus"), + ("BE", "Belgium"), + ("BZ", "Belize"), + ("BJ", "Benin"), + ("BM", "Bermuda"), + ("BT", "Bhutan"), + ("BO", "Bolivia"), + _country_not_found("BQ", "Bonaire, Sint Eustatius and Saba"), + ("BA", "Bosnia and Herzegovina"), + ("BW", "Botswana"), + ("BV", "Bouvet Island"), + ("BR", "Brazil"), + ("IO", "British Indian Ocean Territory"), + ("BN", "Brunei"), + ("BG", "Bulgaria"), + ("BF", "Burkina Faso"), + ("BI", "Burundi"), + ("CV", "Cape Verde"), + ("KH", "Cambodia"), + ("CM", "Cameroon"), + ("CA", "Canada"), + ("KY", "Cayman Islands"), + ("CF", "Central African Republic"), + ("TD", "Chad"), + ("CL", "Chile"), + ("CN", "China"), + ("CX", "Christmas Island"), + ("CC", "Cocos (Keeling) Islands"), + ("CO", "Colombia"), + ("KM", "Comoros"), + ("CD", "Congo (Democratic Republic)"), + ("CG", "Congo"), + ("CK", "Cook Islands"), + ("CR", "Costa Rica"), + ("HR", "Croatia"), + ("CU", "Cuba"), + ("CW", "Curaçao"), + ("CY", "Cyprus"), + ("CZ", "Czechia"), + ("CI", "Ivory Coast"), + ("DK", "Denmark"), + ("DJ", "Djibouti"), + ("DM", "Dominica"), + ("DO", "Dominican Republic"), + ("EC", "Ecuador"), + ("EG", "Egypt"), + ("SV", "El Salvador"), + ("GQ", "Equatorial Guinea"), + ("ER", "Eritrea"), + ("EE", "Estonia"), + ("SZ", "Eswatini"), + ("ET", "Ethiopia"), + ("FK", "Falkland Islands"), + ("FO", "Faroe Islands"), + ("FJ", "Fiji"), + ("FI", "Finland"), + ("FR", "France"), + ("GF", "French Guiana"), + ("PF", "French Polynesia"), + ("TF", "French Southern Territories"), + ("GA", "Gabon"), + ("GM", "The Gambia"), + ("GE", "Georgia"), + ("DE", "Germany"), + ("GH", "Ghana"), + ("GI", "Gibraltar"), + ("GR", "Greece"), + ("GL", "Greenland"), + ("GD", "Grenada"), + ("GP", "Guadeloupe"), + ("GU", "Guam"), + ("GT", "Guatemala"), + ("GG", "United Kingdom"), + ("GN", "Guinea"), + ("GW", "Guinea-Bissau"), + ("GY", "Guyana"), + ("HT", "Haiti"), + ("HM", "Heard Island and McDonald Islands"), + ("VA", "Vatican City"), + ("HN", "Honduras"), + ("HK", "Hong Kong"), + ("HU", "Hungary"), + ("IS", "Iceland"), + ("IN", "India"), + ("ID", "Indonesia"), + ("IR", "Iran"), + ("IQ", "Iraq"), + ("IE", "Ireland"), + ("IM", "United Kingdom"), + ("IL", "Israel"), + ("IT", "Italy"), + ("JM", "Jamaica"), + ("JP", "Japan"), + ("JE", "United Kingdom"), + ("JO", "Jordan"), + ("KZ", "Kazakhstan"), + ("KE", "Kenya"), + ("KI", "Kiribati"), + ("KP", "North Korea"), + ("KR", "South Korea"), + ("KW", "Kuwait"), + ("KG", "Kyrgyzstan"), + ("LA", "Laos"), + ("LV", "Latvia"), + ("LB", "Lebanon"), + ("LS", "Lesotho"), + ("LR", "Liberia"), + ("LY", "Libya"), + ("LI", "Liechtenstein"), + ("LT", "Lithuania"), + ("LU", "Luxembourg"), + ("MO", "Macao"), + ("MG", "Madagascar"), + ("MW", "Malawi"), + ("MY", "Malaysia"), + ("MV", "Maldives"), + ("ML", "Mali"), + ("MT", "Malta"), + ("MH", "Marshall Islands"), + ("MQ", "Martinique"), + ("MR", "Mauritania"), + ("MU", "Mauritius"), + ("YT", "Mayotte"), + ("MX", "Mexico"), + ("FM", "Micronesia"), + ("MD", "Moldova"), + ("MC", "Monaco"), + ("MN", "Mongolia"), + ("ME", "Montenegro"), + ("MS", "Montserrat"), + ("MA", "Morocco"), + ("MZ", "Mozambique"), + ("MM", "Myanmar (Burma)"), + ("NA", "Namibia"), + ("NR", "Nauru"), + ("NP", "Nepal"), + ("NL", "Netherlands"), + ("NC", "New Caledonia"), + ("NZ", "New Zealand"), + ("NI", "United Kingdom"), # NI gets interpreted as ‘Northern Ireland’ + ("NE", "Niger"), + ("NG", "Nigeria"), + ("NU", "Niue"), + ("NF", "Norfolk Island"), + ("MK", "North Macedonia"), + ("MP", "Northern Mariana Islands"), + ("NO", "Norway"), + ("OM", "Oman"), + ("PK", "Pakistan"), + ("PW", "Palau"), + ("PS", "Occupied Palestinian Territories"), + ("PA", "Panama"), + ("PG", "Papua New Guinea"), + ("PY", "Paraguay"), + ("PE", "Peru"), + ("PH", "Philippines"), + ("PN", "Pitcairn, Henderson, Ducie and Oeno Islands"), + ("PL", "Poland"), + ("PT", "Portugal"), + ("PR", "Puerto Rico"), + ("QA", "Qatar"), + ("RO", "Romania"), + ("RU", "Russia"), + ("RW", "Rwanda"), + ("RE", "RÊunion"), + ("BL", "Saint BarthÊlemy"), + _country_not_found("SH", "Saint Helena, Ascension and Tristan da Cunha"), + ("KN", "St Kitts and Nevis"), + ("LC", "St Lucia"), + ("MF", "Saint-Martin (French part)"), + ("PM", "Saint Pierre and Miquelon"), + ("VC", "St Vincent"), + ("WS", "Samoa"), + ("SM", "San Marino"), + ("ST", "Sao Tome and Principe"), + ("SA", "Saudi Arabia"), + ("SN", "Senegal"), + ("RS", "Serbia"), + ("SC", "Seychelles"), + ("SL", "Sierra Leone"), + ("SG", "Singapore"), + ("SX", "Sint Maarten (Dutch part)"), + ("SK", "Slovakia"), + ("SI", "Slovenia"), + ("SB", "Solomon Islands"), + ("SO", "Somalia"), + ("ZA", "South Africa"), + ("GS", "South Georgia and South Sandwich Islands"), + ("SS", "South Sudan"), + ("ES", "Spain"), + ("LK", "Sri Lanka"), + ("SD", "Sudan"), + ("SR", "Suriname"), + ("SJ", "Svalbard and Jan Mayen"), + ("SE", "Sweden"), + ("CH", "Switzerland"), + ("SY", "Syria"), + ("TW", "Taiwan"), + ("TJ", "Tajikistan"), + ("TZ", "Tanzania"), + ("TH", "Thailand"), + ("TL", "East Timor"), + ("TG", "Togo"), + ("TK", "Tokelau"), + ("TO", "Tonga"), + ("TT", "Trinidad and Tobago"), + ("TN", "Tunisia"), + ("TR", "Turkey"), + ("TM", "Turkmenistan"), + ("TC", "Turks and Caicos Islands"), + ("TV", "Tuvalu"), + ("UG", "Uganda"), + ("UA", "Ukraine"), + ("AE", "United Arab Emirates"), + ("GB", "United Kingdom"), + _country_not_found("UM", "United States Minor Outlying Islands"), + ("US", "United States"), + ("UY", "Uruguay"), + ("UZ", "Uzbekistan"), + ("VU", "Vanuatu"), + ("VE", "Venezuela"), + ("VN", "Vietnam"), + ("VG", "British Virgin Islands"), + ("VI", "United States Virgin Islands"), + ("WF", "Wallis and Futuna"), + ("EH", "Western Sahara"), + ("YE", "Yemen"), + ("ZM", "Zambia"), + ("ZW", "Zimbabwe"), + ("AX", "Åland Islands"), + ), +) +def test_iso_alpha_2_country_codes(alpha_2, expected_name): + assert Country(alpha_2).canonical_name == expected_name + + +@pytest.mark.parametrize( + "alpha_3, expected_name", + ( + _country_not_found("AFG", "Afghanistan"), + _country_not_found("ALB", "Albania"), + _country_not_found("DZA", "Algeria"), + _country_not_found("ASM", "American Samoa"), + _country_not_found("AND", "Andorra"), + _country_not_found("AGO", "Angola"), + _country_not_found("AIA", "Anguilla"), + _country_not_found("ATA", "Antarctica"), + _country_not_found("ATG", "Antigua and Barbuda"), + _country_not_found("ARG", "Argentina"), + _country_not_found("ARM", "Armenia"), + _country_not_found("ABW", "Aruba"), + _country_not_found("AUS", "Australia"), + _country_not_found("AUT", "Austria"), + _country_not_found("AZE", "Azerbaijan"), + _country_not_found("BHS", "The Bahamas"), + _country_not_found("BHR", "Bahrain"), + _country_not_found("BGD", "Bangladesh"), + _country_not_found("BRB", "Barbados"), + _country_not_found("BLR", "Belarus"), + _country_not_found("BEL", "Belgium"), + _country_not_found("BLZ", "Belize"), + _country_not_found("BEN", "Benin"), + _country_not_found("BMU", "Bermuda"), + _country_not_found("BTN", "Bhutan"), + _country_not_found("BOL", "Bolivia"), + _country_not_found("BES", "Bonaire, Sint Eustatius and Saba"), + ("BIH", "Bosnia and Herzegovina"), + _country_not_found("BWA", "Botswana"), + _country_not_found("BVT", "Bouvet Island"), + _country_not_found("BRA", "Brazil"), + ("IOT", "British Indian Ocean Territory"), + _country_not_found("BRN", "Brunei"), + _country_not_found("BGR", "Bulgaria"), + _country_not_found("BFA", "Burkina Faso"), + _country_not_found("BDI", "Burundi"), + _country_not_found("CPV", "Cape Verde"), + _country_not_found("KHM", "Cambodia"), + _country_not_found("CMR", "Cameroon"), + _country_not_found("CAN", "Canada"), + _country_not_found("CYM", "Cayman Islands"), + _country_not_found("CAF", "Central African Republic"), + _country_not_found("TCD", "Chad"), + _country_not_found("CHL", "Chile"), + _country_not_found("CHN", "China"), + _country_not_found("CXR", "Christmas Island"), + _country_not_found("CCK", "Cocos (Keeling) Islands"), + _country_not_found("COL", "Colombia"), + _country_not_found("COM", "Comoros"), + _country_not_found("COD", "Congo (Democratic Republic)"), + _country_not_found("COG", "Congo"), + _country_not_found("COK", "Cook Islands"), + _country_not_found("CRI", "Costa Rica"), + _country_not_found("HRV", "Croatia"), + _country_not_found("CUB", "Cuba"), + _country_not_found("CUW", "Curaçao"), + _country_not_found("CYP", "Cyprus"), + _country_not_found("CZE", "Czechia"), + _country_not_found("CIV", "Ivory Coast"), + _country_not_found("DNK", "Denmark"), + _country_not_found("DJI", "Djibouti"), + _country_not_found("DMA", "Dominica"), + _country_not_found("DOM", "Dominican Republic"), + _country_not_found("ECU", "Ecuador"), + _country_not_found("EGY", "Egypt"), + _country_not_found("SLV", "El Salvador"), + _country_not_found("GNQ", "Equatorial Guinea"), + _country_not_found("ERI", "Eritrea"), + _country_not_found("EST", "Estonia"), + _country_not_found("SWZ", "Eswatini"), + _country_not_found("ETH", "Ethiopia"), + _country_not_found("FLK", "Falkland Islands"), + _country_not_found("FRO", "Faroe Islands"), + _country_not_found("FJI", "Fiji"), + _country_not_found("FIN", "Finland"), + _country_not_found("FRA", "France"), + _country_not_found("GUF", "French Guiana"), + _country_not_found("PYF", "French Polynesia"), + _country_not_found("ATF", "French Southern Territories"), + _country_not_found("GAB", "Gabon"), + _country_not_found("GMB", "The Gambia"), + _country_not_found("GEO", "Georgia"), + _country_not_found("DEU", "Germany"), + _country_not_found("GHA", "Ghana"), + _country_not_found("GIB", "Gibraltar"), + _country_not_found("GRC", "Greece"), + _country_not_found("GRL", "Greenland"), + _country_not_found("GRD", "Grenada"), + _country_not_found("GLP", "Guadeloupe"), + _country_not_found("GUM", "Guam"), + _country_not_found("GTM", "Guatemala"), + _country_not_found("GGY", "United Kingdom"), + _country_not_found("GIN", "Guinea"), + _country_not_found("GNB", "Guinea-Bissau"), + _country_not_found("GUY", "Guyana"), + _country_not_found("HTI", "Haiti"), + _country_not_found("HMD", "Heard Island and McDonald Islands"), + _country_not_found("VAT", "Vatican City"), + _country_not_found("HND", "Honduras"), + _country_not_found("HKG", "Hong Kong"), + _country_not_found("HUN", "Hungary"), + _country_not_found("ISL", "Iceland"), + _country_not_found("IND", "India"), + _country_not_found("IDN", "Indonesia"), + _country_not_found("IRN", "Iran"), + _country_not_found("IRQ", "Iraq"), + _country_not_found("IRL", "Ireland"), + _country_not_found("IMN", "United Kingdom"), + _country_not_found("ISR", "Israel"), + _country_not_found("ITA", "Italy"), + _country_not_found("JAM", "Jamaica"), + _country_not_found("JPN", "Japan"), + _country_not_found("JEY", "United Kingdom"), + _country_not_found("JOR", "Jordan"), + _country_not_found("KAZ", "Kazakhstan"), + _country_not_found("KEN", "Kenya"), + _country_not_found("KIR", "Kiribati"), + ("PRK", "North Korea"), + _country_not_found("KOR", "Korea"), + _country_not_found("KWT", "Kuwait"), + _country_not_found("KGZ", "Kyrgyzstan"), + ("LAO", "Laos"), + _country_not_found("LVA", "Latvia"), + _country_not_found("LBN", "Lebanon"), + _country_not_found("LSO", "Lesotho"), + _country_not_found("LBR", "Liberia"), + _country_not_found("LBY", "Libya"), + _country_not_found("LIE", "Liechtenstein"), + _country_not_found("LTU", "Lithuania"), + _country_not_found("LUX", "Luxembourg"), + _country_not_found("MAC", "Macao"), + _country_not_found("MDG", "Madagascar"), + _country_not_found("MWI", "Malawi"), + _country_not_found("MYS", "Malaysia"), + _country_not_found("MDV", "Maldives"), + _country_not_found("MLI", "Mali"), + _country_not_found("MLT", "Malta"), + _country_not_found("MHL", "Marshall Islands"), + _country_not_found("MTQ", "Martinique"), + _country_not_found("MRT", "Mauritania"), + _country_not_found("MUS", "Mauritius"), + _country_not_found("MYT", "Mayotte"), + _country_not_found("MEX", "Mexico"), + _country_not_found("FSM", "Micronesia"), + _country_not_found("MDA", "Moldova"), + _country_not_found("MCO", "Monaco"), + _country_not_found("MNG", "Mongolia"), + _country_not_found("MNE", "Montenegro"), + _country_not_found("MSR", "Montserrat"), + _country_not_found("MAR", "Morocco"), + _country_not_found("MOZ", "Mozambique"), + _country_not_found("MMR", "Myanmar (Burma)"), + _country_not_found("NAM", "Namibia"), + _country_not_found("NRU", "Nauru"), + _country_not_found("NPL", "Nepal"), + _country_not_found("NLD", "Netherlands"), + _country_not_found("NCL", "New Caledonia"), + _country_not_found("NZL", "New Zealand"), + _country_not_found("NIC", "Nicaragua"), + _country_not_found("NER", "Niger"), + _country_not_found("NGA", "Nigeria"), + _country_not_found("NIU", "Niue"), + _country_not_found("NFK", "Norfolk Island"), + _country_not_found("MKD", "North Macedonia"), + _country_not_found("MNP", "Northern Mariana Islands"), + _country_not_found("NOR", "Norway"), + _country_not_found("OMN", "Oman"), + _country_not_found("PAK", "Pakistan"), + _country_not_found("PLW", "Palau"), + _country_not_found("PSE", "Occupied Palestinian Territories"), + _country_not_found("PAN", "Panama"), + ("PNG", "Papua New Guinea"), + _country_not_found("PRY", "Paraguay"), + _country_not_found("PER", "Peru"), + _country_not_found("PHL", "Philippines"), + _country_not_found("PCN", "Pitcairn, Henderson, Ducie and Oeno Islands"), + _country_not_found("POL", "Poland"), + _country_not_found("PRT", "Portugal"), + _country_not_found("PRI", "Puerto Rico"), + _country_not_found("QAT", "Qatar"), + _country_not_found("ROU", "Romania"), + _country_not_found("RUS", "Russian Federation"), + _country_not_found("RWA", "Rwanda"), + _country_not_found("REU", "RÊunion"), + _country_not_found("BLM", "Saint BarthÊlemy"), + _country_not_found("SHN", "Saint Helena, Ascension and Tristan da Cunha"), + _country_not_found("KNA", "St Kitts and Nevis"), + _country_not_found("LCA", "St Lucia"), + _country_not_found("MAF", "Saint-Martin (French part)"), + _country_not_found("SPM", "Saint Pierre and Miquelon"), + _country_not_found("VCT", "Saint Vincent"), + _country_not_found("WSM", "Samoa"), + _country_not_found("SMR", "San Marino"), + _country_not_found("STP", "Sao Tome and Principe"), + _country_not_found("SAU", "Saudi Arabia"), + _country_not_found("SEN", "Senegal"), + _country_not_found("SRB", "Serbia"), + _country_not_found("SYC", "Seychelles"), + _country_not_found("SLE", "Sierra Leone"), + _country_not_found("SGP", "Singapore"), + _country_not_found("SXM", "Sint Maarten (Dutch part)"), + _country_not_found("SVK", "Slovakia"), + _country_not_found("SVN", "Slovenia"), + _country_not_found("SLB", "Solomon Islands"), + _country_not_found("SOM", "Somalia"), + _country_not_found("ZAF", "South Africa"), + _country_not_found("SGS", "South Georgia and South Sandwich Islands"), + _country_not_found("SSD", "South Sudan"), + _country_not_found("ESP", "Spain"), + _country_not_found("LKA", "Sri Lanka"), + _country_not_found("SDN", "Sudan"), + _country_not_found("SUR", "Suriname"), + _country_not_found("SJM", "Svalbard and Jan Mayen"), + _country_not_found("SWE", "Sweden"), + _country_not_found("CHE", "Switzerland"), + _country_not_found("SYR", "Syrian Arab Republic"), + _country_not_found("TWN", "Taiwan"), + _country_not_found("TJK", "Tajikistan"), + _country_not_found("TZA", "Tanzania"), + _country_not_found("THA", "Thailand"), + _country_not_found("TLS", "East Timor"), + _country_not_found("TGO", "Togo"), + _country_not_found("TKL", "Tokelau"), + _country_not_found("TON", "Tonga"), + _country_not_found("TTO", "Trinidad and Tobago"), + _country_not_found("TUN", "Tunisia"), + _country_not_found("TUR", "Turkey"), + _country_not_found("TKM", "Turkmenistan"), + _country_not_found("TCA", "Turks and Caicos Islands"), + _country_not_found("TUV", "Tuvalu"), + _country_not_found("UGA", "Uganda"), + _country_not_found("UKR", "Ukraine"), + _country_not_found("ARE", "United Arab Emirates"), + ("GBR", "United Kingdom"), + _country_not_found("UMI", "United States Minor Outlying Islands"), + ("USA", "United States"), + _country_not_found("URY", "Uruguay"), + _country_not_found("UZB", "Uzbekistan"), + _country_not_found("VUT", "Vanuatu"), + _country_not_found("VEN", "Venezuela"), + _country_not_found("VNM", "Vietnam"), + _country_not_found("VGB", "British Virgin Islands"), + _country_not_found("VIR", "United States Virgin Islands"), + _country_not_found("WLF", "Wallis and Futuna"), + _country_not_found("ESH", "Western Sahara"), + _country_not_found("YEM", "Yemen"), + _country_not_found("ZMB", "Zambia"), + _country_not_found("ZWE", "Zimbabwe"), + _country_not_found("ALA", "Åland Islands"), + ), +) +def test_iso_alpha_3_country_codes(alpha_3, expected_name): + assert Country(alpha_3).canonical_name == expected_name diff --git a/tests/notification_utils/test_field.py b/tests/notification_utils/test_field.py new file mode 100644 index 000000000..686556b68 --- /dev/null +++ b/tests/notification_utils/test_field.py @@ -0,0 +1,311 @@ +import pytest + +from notifications_utils.field import Field, str2bool + + +@pytest.mark.parametrize( + "content", + [ + "", + "the quick brown fox", + """ + the + quick brown + + fox + """, + "the ((quick brown fox", + "the (()) brown fox", + ], +) +def test_returns_a_string_without_placeholders(content): + assert str(Field(content)) == content + + +@pytest.mark.parametrize( + "template_content,data,expected", + [ + ("((colour))", {"colour": "red"}, "red"), + ("the quick ((colour)) fox", {"colour": "brown"}, "the quick brown fox"), + ( + "((article)) quick ((colour)) ((animal))", + {"article": "the", "colour": "brown", "animal": "fox"}, + "the quick brown fox", + ), + ("the quick (((colour))) fox", {"colour": "brown"}, "the quick (brown) fox"), + ( + "the quick ((colour)) fox", + {"colour": ""}, + "the quick alert('foo') fox", + ), + ( + "before ((placeholder)) after", + {"placeholder": ""}, + "before after", + ), + ( + "before ((placeholder)) after", + {"placeholder": " "}, + "before after", + ), + ( + "before ((placeholder)) after", + {"placeholder": True}, + "before True after", + ), + ( + "before ((placeholder)) after", + {"placeholder": False}, + "before False after", + ), + ( + "before ((placeholder)) after", + {"placeholder": 0}, + "before 0 after", + ), + ( + "before ((placeholder)) after", + {"placeholder": 0.0}, + "before 0.0 after", + ), + ( + "before ((placeholder)) after", + {"placeholder": 123}, + "before 123 after", + ), + ( + "before ((placeholder)) after", + {"placeholder": 0.1 + 0.2}, + "before 0.30000000000000004 after", + ), + ( + "before ((placeholder)) after", + {"placeholder": {"key": "value"}}, + "before {'key': 'value'} after", + ), + ( + "((warning?))", + {"warning?": "This is not a conditional"}, + "This is not a conditional", + ), + ( + "((warning?warning))", + {"warning?warning": "This is not a conditional"}, + "This is not a conditional", + ), + ( + "((warning??This is a conditional warning))", + {"warning": True}, + "This is a conditional warning", + ), + ( + "((warning??This is a conditional warning\nwith line break))", + {"warning": True}, + "This is a conditional warning\nwith line break", + ), + ("((warning??This is a conditional warning))", {"warning": False}, ""), + ], +) +def test_replacement_of_placeholders(template_content, data, expected): + assert str(Field(template_content, data)) == expected + + +@pytest.mark.parametrize( + "template_content,data,expected", + [ + ( + "((code)) is your security code", + {"code": "12345"}, + "12345 is your security code", + ), + ( + "((code)) is your security code", + {}, + "hidden is your security code", + ), + ( + "Hey ((name)), click http://example.com/reset-password/?token=((token))", + {"name": "Example"}, + ( + "Hey Example, click " + "http://example.com/reset-password/?token=" + "hidden" + ), + ), + ], +) +def test_optional_redacting_of_missing_values(template_content, data, expected): + assert ( + str(Field(template_content, data, redact_missing_personalisation=True)) + == expected + ) + + +@pytest.mark.parametrize( + "content,expected", + [ + ("((colour))", "((colour))"), + ( + "the quick ((colour)) fox", + "the quick ((colour)) fox", + ), + ( + "((article)) quick ((colour)) ((animal))", + "((article)) quick ((colour)) ((animal))", # noqa + ), + ( + """ + ((article)) quick + ((colour)) + ((animal)) + """, + """ + ((article)) quick + ((colour)) + ((animal)) + """, + ), + ( + "the quick (((colour))) fox", + "the quick (((colour))) fox", + ), + ("((warning?))", "((warning?))"), + ( + "((warning? This is not a conditional))", + "((warning? This is not a conditional))", + ), + ( + "((warning?? This is a warning))", + "((warning?? This is a warning))", + ), + ( + "((warning?? This is a warning\n text after linebreak))", + "((warning?? This is a warning\n text after linebreak))", + ), + ], +) +def test_formatting_of_placeholders(content, expected): + assert str(Field(content)) == expected + + +@pytest.mark.parametrize( + "content, values, expected", + [ + ( + "((name)) ((colour))", + {"name": "Jo"}, + "Jo ((colour))", + ), + ( + "((name)) ((colour))", + {"name": "Jo", "colour": None}, + "Jo ((colour))", + ), + ( + "((show_thing??thing)) ((colour))", + {"colour": "red"}, + "((show_thing??thing)) red", + ), + ], +) +def test_handling_of_missing_values(content, values, expected): + assert str(Field(content, values)) == expected + + +@pytest.mark.parametrize( + "value", + [ + "0", + 0, + 2, + 99.99999, + "off", + "exclude", + "no" "any random string", + "false", + False, + [], + {}, + (), + ["true"], + {"True": True}, + (True, "true", 1), + ], +) +def test_what_will_not_trigger_conditional_placeholder(value): + assert str2bool(value) is False + + +@pytest.mark.parametrize( + "value", [1, "1", "yes", "y", "true", "True", True, "include", "show"] +) +def test_what_will_trigger_conditional_placeholder(value): + assert str2bool(value) is True + + +@pytest.mark.parametrize( + "values, expected, expected_as_markdown", + [ + ( + {"placeholder": []}, + "list: ", + "list: ", + ), + ( + {"placeholder": ["", ""]}, + "list: ", + "list: ", + ), + ( + {"placeholder": [" ", " \t ", "\u180E"]}, + "list: ", + "list: ", + ), + ( + {"placeholder": ["one"]}, + "list: one", + "list: \n\n* one", + ), + ( + {"placeholder": ["one", "two"]}, + "list: one and two", + "list: \n\n* one\n* two", + ), + ( + {"placeholder": ["one", "two", "three"]}, + "list: one, two and three", + "list: \n\n* one\n* two\n* three", + ), + ( + {"placeholder": ["one", None, None]}, + "list: one", + "list: \n\n* one", + ), + ( + {"placeholder": [""]}, + 'list: , alert("foo") and ', + 'list: \n\n* \n* alert("foo")\n* ', + ), + ( + {"placeholder": [1, {"two": 2}, "three", None]}, + "list: 1, {'two': 2} and three", + "list: \n\n* 1\n* {'two': 2}\n* three", + ), + ( + {"placeholder": [[1, 2], [3, 4]]}, + "list: [1, 2] and [3, 4]", + "list: \n\n* [1, 2]\n* [3, 4]", + ), + ( + {"placeholder": [0.1, True, False]}, + "list: 0.1, True and False", + "list: \n\n* 0.1\n* True\n* False", + ), + ], +) +def test_field_renders_lists_as_strings(values, expected, expected_as_markdown): + assert ( + str(Field("list: ((placeholder))", values, markdown_lists=True)) + == expected_as_markdown + ) + assert str(Field("list: ((placeholder))", values)) == expected diff --git a/tests/notification_utils/test_field_html_handling.py b/tests/notification_utils/test_field_html_handling.py new file mode 100644 index 000000000..d113f816c --- /dev/null +++ b/tests/notification_utils/test_field_html_handling.py @@ -0,0 +1,71 @@ +import pytest + +from notifications_utils.field import Field + + +@pytest.mark.parametrize( + "content, values, expected_stripped, expected_escaped, expected_passthrough", + [ + ( + "string with html", + {}, + "string with html", + "string <em>with</em> html", + "string with html", + ), + ( + "string ((with)) html", + {}, + "string ((with)) html", + "string ((<em>with</em>)) html", + "string ((with)) html", + ), + ( + "string ((placeholder)) html", + {"placeholder": "without"}, + "string without html", + "string <em>without</em> html", + "string without html", + ), + ( + "string ((conditional??placeholder)) html", + {}, + "string ((conditional??placeholder)) html", + ( + "string " + "" + "((<em>conditional</em>??" + "<em>placeholder</em>)) " + "html" + ), + ( + "string " + "" + "((conditional??" + "placeholder)) " + "html" + ), + ), + ( + "string ((conditional??placeholder)) html", + {"conditional": True}, + "string placeholder html", + "string <em>placeholder</em> html", + "string placeholder html", + ), + ( + "string & entity", + {}, + "string & entity", + "string & entity", + "string & entity", + ), + ], +) +def test_field_handles_html( + content, values, expected_stripped, expected_escaped, expected_passthrough +): + assert str(Field(content, values)) == expected_stripped + assert str(Field(content, values, html="strip")) == expected_stripped + assert str(Field(content, values, html="escape")) == expected_escaped + assert str(Field(content, values, html="passthrough")) == expected_passthrough diff --git a/tests/notification_utils/test_files/multi_page_pdf.pdf b/tests/notification_utils/test_files/multi_page_pdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..99d31cef1efdbb90fba2231a19974f4f74229be0 GIT binary patch literal 54836 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://example.com/"onclick="alert('hi')""", # noqa + ), + ( + """https://example.com/"style='text-decoration:blink'""", + """https://example.com/"style='text-decoration:blink'""", # noqa + ), + ], +) +def test_URLs_get_escaped_in_sms(url, expected_html): + assert expected_html in str( + SMSPreviewTemplate({"content": url, "template_type": "sms"}) + ) + + +def test_HTML_template_has_URLs_replaced_with_links(): + assert ( + '' + "https://service.example.com/accept_invite/a1b2c3d4" + "" + ) in str( + HTMLEmailTemplate( + { + "content": ( + "You’ve been invited to a service. Click this link:\n" + "https://service.example.com/accept_invite/a1b2c3d4\n" + "\n" + "Thanks\n" + ), + "subject": "", + "template_type": "email", + } + ) + ) + + +def test_escaping_govuk_in_email_templates(): + template_content = "GOV.UK" + expected = "GOV.\u200BUK" + assert unlink_govuk_escaped(template_content) == expected + template_json = { + "content": template_content, + "subject": "", + "template_type": "email", + } + assert expected in str(PlainTextEmailTemplate(template_json)) + assert expected in str(HTMLEmailTemplate(template_json)) + + +@pytest.mark.parametrize( + "template_content,expected", + [ + # Cases that we add the breaking space + ("GOV.UK", "GOV.\u200BUK"), + ("gov.uk", "gov.\u200Buk"), + ( + "content with space infront GOV.UK", + "content with space infront GOV.\u200BUK", + ), + ("content with tab infront\tGOV.UK", "content with tab infront\tGOV.\u200BUK"), + ( + "content with newline infront\nGOV.UK", + "content with newline infront\nGOV.\u200BUK", + ), + ("*GOV.UK", "*GOV.\u200BUK"), + ("#GOV.UK", "#GOV.\u200BUK"), + ("^GOV.UK", "^GOV.\u200BUK"), + (" #GOV.UK", " #GOV.\u200BUK"), + ("GOV.UK with CONTENT after", "GOV.\u200BUK with CONTENT after"), + ("#GOV.UK with CONTENT after", "#GOV.\u200BUK with CONTENT after"), + # Cases that we don't add the breaking space + ("https://gov.uk", "https://gov.uk"), + ("https://www.gov.uk", "https://www.gov.uk"), + ("www.gov.uk", "www.gov.uk"), + ("WWW.GOV.UK", "WWW.GOV.UK"), + ("WWW.GOV.UK.", "WWW.GOV.UK."), + ( + "https://www.gov.uk/?utm_source=gov.uk", + "https://www.gov.uk/?utm_source=gov.uk", + ), + ("mygov.uk", "mygov.uk"), + ("www.this-site-is-not-gov.uk", "www.this-site-is-not-gov.uk"), + ( + "www.gov.uk?websites=bbc.co.uk;gov.uk;nsh.scot", + "www.gov.uk?websites=bbc.co.uk;gov.uk;nsh.scot", + ), + ("reply to: xxxx@xxx.gov.uk", "reply to: xxxx@xxx.gov.uk"), + ("southwark.gov.uk", "southwark.gov.uk"), + ("data.gov.uk", "data.gov.uk"), + ("gov.uk/foo", "gov.uk/foo"), + ("*GOV.UK/foo", "*GOV.UK/foo"), + ("#GOV.UK/foo", "#GOV.UK/foo"), + ("^GOV.UK/foo", "^GOV.UK/foo"), + ("gov.uk#departments-and-policy", "gov.uk#departments-and-policy"), + # Cases that we know currently aren't supported by our regex and have a non breaking space added when they + # shouldn't however, we accept the fact that our regex isn't perfect as we think the chance of a user using a + # URL like this in their content is very small. + # We document these edge cases here + pytest.param("gov.uk.com", "gov.uk.com", marks=pytest.mark.xfail), + pytest.param("gov.ukandi.com", "gov.ukandi.com", marks=pytest.mark.xfail), + pytest.param("gov.uks", "gov.uks", marks=pytest.mark.xfail), + ], +) +def test_unlink_govuk_escaped(template_content, expected): + assert unlink_govuk_escaped(template_content) == expected + + +@pytest.mark.parametrize( + "prefix, body, expected", + [ + ("a", "b", "a: b"), + (None, "b", "b"), + ], +) +def test_sms_message_adds_prefix(prefix, body, expected): + template = SMSMessageTemplate({"content": body, "template_type": "sms"}) + template.prefix = prefix + template.sender = None + assert str(template) == expected + + +def test_sms_preview_adds_newlines(): + template = SMSPreviewTemplate( + { + "content": """ + the + quick + + brown fox + """, + "template_type": "sms", + } + ) + template.prefix = None + template.sender = None + assert "
    " in str(template) + + +def test_sms_encode(mocker): + sanitise_mock = mocker.patch("notifications_utils.formatters.SanitiseSMS") + assert sms_encode("foo") == sanitise_mock.encode.return_value + sanitise_mock.encode.assert_called_once_with("foo") + + +@pytest.mark.parametrize( + "items, kwargs, expected_output", + [ + ([1], {}, "‘1’"), + ([1, 2], {}, "‘1’ and ‘2’"), + ([1, 2, 3], {}, "‘1’, ‘2’ and ‘3’"), + ([1, 2, 3], {"prefix": "foo", "prefix_plural": "bar"}, "bar ‘1’, ‘2’ and ‘3’"), + ([1], {"prefix": "foo", "prefix_plural": "bar"}, "foo ‘1’"), + ([1, 2, 3], {"before_each": "a", "after_each": "b"}, "a1b, a2b and a3b"), + ([1, 2, 3], {"conjunction": "foo"}, "‘1’, ‘2’ foo ‘3’"), + (["&"], {"before_each": "", "after_each": ""}, "&"), + ( + [1, 2, 3], + {"before_each": "", "after_each": ""}, + "1, 2 and 3", + ), + ], +) +def test_formatted_list(items, kwargs, expected_output): + assert formatted_list(items, **kwargs) == expected_output + + +def test_formatted_list_returns_markup(): + assert isinstance(formatted_list([0]), Markup) + + +def test_bleach_doesnt_try_to_make_valid_html_before_cleaning(): + assert escape_html("") == ( + "<to cancel daily cat facts reply 'cancel'>" + ) + + +@pytest.mark.parametrize( + "content, expected_escaped", + ( + ("&?a;", "&?a;"), + ("&>a;", "&>a;"), + ("&*a;", "&*a;"), + ("&a?;", "&a?;"), + ("&x?xa;", "&x?xa;"), + # We need to be careful that query arguments don’t get turned into entities + ("×tamp=×", "&timestamp=×"), + ("×=1,2,3", "&times=1,2,3"), + # − should have a trailing semicolon according to the HTML5 + # spec but µ doesn’t need one + ("2−1", "2−1"), + ("200µg", "200Âĩg"), + # â€Ļwe ignore it when it’s ambiguous + ("2&minus1", "2&minus1"), + ("200µg", "200&microg"), + # â€Ļwe still ignore when there’s a space afterwards + ("2 &minus 1", "2 &minus 1"), + ("200µ g", "200&micro g"), + # Things which aren’t real entities are ignored, not removed + ("This &isnotarealentity;", "This &isnotarealentity;"), + # We let users use   for backwards compatibility + ("Before after", "Before after"), + # We let users use & because it’s often pasted in URLs + ("?a=1&b=2", "?a=1&b=2"), + # We let users use ( and ) because otherwise it’s + # impossible to put brackets in the body of conditional placeholders + ("((var??(in brackets)))", "((var??(in brackets)))"), + ), +) +def test_escaping_html_entities( + content, + expected_escaped, +): + assert escape_html(content) == expected_escaped + + +@pytest.mark.parametrize( + "dirty, clean", + [ + ( + "Hello ((name)) ,\n\nThis is a message", + "Hello ((name)),\n\nThis is a message", + ), + ("Hello Jo ,\n\nThis is a message", "Hello Jo,\n\nThis is a message"), + ( + "\n \t , word", + "\n, word", + ), + ], +) +def test_removing_whitespace_before_commas(dirty, clean): + assert remove_whitespace_before_punctuation(dirty) == clean + + +@pytest.mark.parametrize( + "dirty, clean", + [ + ( + "Hello ((name)) .\n\nThis is a message", + "Hello ((name)).\n\nThis is a message", + ), + ("Hello Jo .\n\nThis is a message", "Hello Jo.\n\nThis is a message"), + ( + "\n \t . word", + "\n. word", + ), + ], +) +def test_removing_whitespace_before_full_stops(dirty, clean): + assert remove_whitespace_before_punctuation(dirty) == clean + + +@pytest.mark.parametrize( + "dumb, smart", + [ + ( + """And I said, "what about breakfast at Tiffany's"?""", + """And I said, “what about breakfast at Tiffany’s”?""", + ), + ( + """ + http://example.com?q='foo' + """, + """ + http://example.com?q='foo' + """, + ), + ], +) +def test_smart_quotes(dumb, smart): + assert make_quotes_smart(dumb) == smart + + +@pytest.mark.parametrize( + "nasty, nice", + [ + ( + ( + "The en dash - always with spaces in running text when, as " + "discussed in this section, indicating a parenthesis or " + "pause - and the spaced em dash both have a certain " + "technical advantage over the unspaced em dash. " + ), + ( + "The en dash \u2013 always with spaces in running text when, as " + "discussed in this section, indicating a parenthesis or " + "pause \u2013 and the spaced em dash both have a certain " + "technical advantage over the unspaced em dash. " + ), + ), + ( + "double -- dash", + "double \u2013 dash", + ), + ( + "triple --- dash", + "triple \u2013 dash", + ), + ( + "quadruple ---- dash", + "quadruple ---- dash", + ), + ( + "em — dash", + "em – dash", + ), + ( + "already\u0020–\u0020correct", # \u0020 is a normal space character + "already\u0020–\u0020correct", + ), + ( + "2004-2008", + "2004-2008", # no replacement + ), + ], +) +def test_en_dashes(nasty, nice): + assert replace_hyphens_with_en_dashes(nasty) == nice + + +def test_unicode_dash_lookup(): + en_dash_replacement_sequence = "\u0020\u2013" + hyphen = "-" + en_dash = "–" + space = " " + non_breaking_space = " " + assert en_dash_replacement_sequence == space + en_dash + assert non_breaking_space not in en_dash_replacement_sequence + assert hyphen not in en_dash_replacement_sequence + + +@pytest.mark.parametrize( + "value", + [ + "bar", + " bar ", + """ + \t bar + """, + " \u180E\u200B \u200C bar \u200D \u2060\uFEFF ", + ], +) +def test_strip_all_whitespace(value): + assert strip_all_whitespace(value) == "bar" + + +@pytest.mark.parametrize( + "value", + [ + "notifications-email", + " \tnotifications-email \x0c ", + "\rn\u200Coti\u200Dfi\u200Bcati\u2060ons-\u180Eemai\uFEFFl\uFEFF", + ], +) +def test_strip_and_remove_obscure_whitespace(value): + assert strip_and_remove_obscure_whitespace(value) == "notifications-email" + + +def test_strip_and_remove_obscure_whitespace_only_removes_normal_whitespace_from_ends(): + sentence = " words \n over multiple lines with \ttabs\t " + assert ( + strip_and_remove_obscure_whitespace(sentence) + == "words \n over multiple lines with \ttabs" + ) + + +def test_remove_smart_quotes_from_email_addresses(): + assert ( + remove_smart_quotes_from_email_addresses( + """ + line one’s quote + first.o’last@example.com is someone’s email address + line ‘three’ + """ + ) + == ( + """ + line one’s quote + first.o'last@example.com is someone’s email address + line ‘three’ + """ + ) + ) + + +def test_strip_unsupported_characters(): + assert strip_unsupported_characters("line one\u2028line two") == ( + "line oneline two" + ) + + +@pytest.mark.parametrize( + "value", + [ + "\u200C Your tax is\ndue\n\n", + " Your tax is due ", + # Non breaking spaces replaced by single spaces + "\u00A0Your\u00A0tax\u00A0 is\u00A0\u00A0due\u00A0", + # zero width spaces are removed + "\u180EYour \u200Btax\u200C is \u200D\u2060due \uFEFF", + # tabs are replaced by single spaces + "\tYour tax\tis due ", + ], +) +def test_normalise_whitespace(value): + assert normalise_whitespace(value) == "Your tax is due" + + +@pytest.mark.parametrize( + "content, expected_html", + ( + ( + "http://example.com", + 'http://example.com', + ), + ( + "https://example.com", + 'https://example.com', + ), + ( + "example.com", + 'example.com', + ), + ( + "www.foo.bar.example.com", + 'www.foo.bar.example.com', + ), + ( + "example.com/", + 'example.com/', + ), + ( + "www.foo.bar.example.com/", + 'www.foo.bar.example.com/', + ), + ( + "example.com/foo", + 'example.com/foo', + ), + ( + "example.com?foo", + 'example.com?foo', + ), + ( + "example.com#foo", + 'example.com#foo', + ), + ( + "Go to gov.uk/example.", + "Go to " 'gov.uk/example.', + ), + ( + "Go to gov.uk/example:", + "Go to " 'gov.uk/example:', + ), + ( + "Go to gov.uk/example;", + "Go to " 'gov.uk/example;', + ), + ( + "(gov.uk/example)", + "(" 'gov.uk/example)', + ), + ( + "(gov.uk/example)...", + "(" 'gov.uk/example)...', + ), + ( + "(gov.uk/example.)", + "(" 'gov.uk/example.)', + ), + ( + "(see example.com/foo_(bar))", + "(see " + 'example.com/foo_(bar))', + ), + ( + "example.com/foo(((((((bar", + 'example.com/foo(((((((bar', + ), + ( + "government website (gov.uk). Other websitesâ€Ļ", + "government website (" + 'gov.uk). Other websitesâ€Ļ', + ), + ( + "[gov.uk/example]", + "[" 'gov.uk/example]', + ), + ( + "gov.uk/foo, gov.uk/bar", + 'gov.uk/foo, ' + 'gov.uk/bar', + ), + ( + "

    gov.uk/foo

    ", + "

    " 'gov.uk/foo

    ', + ), + ( + "gov.uk?foo&", + 'gov.uk?foo&', + ), + ( + "a .service.gov.uk domain", + "a .service.gov.uk domain", + ), + ( + 'http://foo.com/"bar"?x=1#2', + 'http://foo.com/"bar"?x=1#2', + ), + ( + "firstname.lastname@example.com", + "firstname.lastname@example.com", + ), + ( + "with-subdomain@test.example.com", + "with-subdomain@test.example.com", + ), + ), +) +def test_autolink_urls_matches_correctly(content, expected_html): + assert autolink_urls(content) == expected_html + + +@pytest.mark.parametrize( + "extra_kwargs, expected_html", + ( + ( + {}, + 'http://example.com', + ), + ( + { + "classes": "govuk-link", + }, + 'http://example.com', + ), + ), +) +def test_autolink_urls_applies_correct_attributes(extra_kwargs, expected_html): + assert autolink_urls("http://example.com", **extra_kwargs) == expected_html + + +@pytest.mark.parametrize( + "content", ("without link", "with link to https://example.com") +) +def test_autolink_urls_returns_markup(content): + assert isinstance(autolink_urls(content), Markup) diff --git a/tests/notification_utils/test_insensitive_dict.py b/tests/notification_utils/test_insensitive_dict.py new file mode 100644 index 000000000..ae7f62f9c --- /dev/null +++ b/tests/notification_utils/test_insensitive_dict.py @@ -0,0 +1,96 @@ +from functools import partial + +import pytest + +from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.recipients import Cell, Row + + +def test_columns_as_dict_with_keys(): + assert InsensitiveDict( + {"Date of Birth": "01/01/2001", "TOWN": "London"} + ).as_dict_with_keys({"date_of_birth", "town"}) == { + "date_of_birth": "01/01/2001", + "town": "London", + } + + +def test_columns_as_dict(): + assert dict(InsensitiveDict({"date of birth": "01/01/2001", "TOWN": "London"})) == { + "dateofbirth": "01/01/2001", + "town": "London", + } + + +def test_missing_data(): + partial_row = partial( + Row, + row_dict={}, + index=1, + error_fn=None, + recipient_column_headers=[], + placeholders=[], + template=None, + allow_international_letters=False, + ) + with pytest.raises(KeyError): + InsensitiveDict({})["foo"] + assert InsensitiveDict({}).get("foo") is None + assert InsensitiveDict({}).get("foo", "bar") == "bar" + assert partial_row()["foo"] == Cell() + assert partial_row().get("foo") == Cell() + assert partial_row().get("foo", "bar") == "bar" + + +@pytest.mark.parametrize( + "in_dictionary", + [ + {"foo": "bar"}, + {"F_O O": "bar"}, + ], +) +@pytest.mark.parametrize( + "key, should_be_present", + [ + ("foo", True), + ("f_o_o", True), + ("F O O", True), + ("bar", False), + ], +) +def test_lookup(key, should_be_present, in_dictionary): + assert (key in InsensitiveDict(in_dictionary)) == should_be_present + + +@pytest.mark.parametrize( + "key_in", + [ + "foo", + "F_O O", + ], +) +@pytest.mark.parametrize( + "lookup_key", + [ + "foo", + "f_o_o", + "F O O", + ], +) +def test_set_item(key_in, lookup_key): + columns = InsensitiveDict({}) + columns[key_in] = "bar" + assert columns[lookup_key] == "bar" + + +def test_maintains_insertion_order(): + d = InsensitiveDict( + { + "B": None, + "A": None, + "C": None, + } + ) + assert d.keys() == ["b", "a", "c"] + d["BB"] = None + assert d.keys() == ["b", "a", "c", "bb"] diff --git a/tests/notification_utils/test_international_billing_rates.py b/tests/notification_utils/test_international_billing_rates.py new file mode 100644 index 000000000..5e68d767b --- /dev/null +++ b/tests/notification_utils/test_international_billing_rates.py @@ -0,0 +1,50 @@ +import pytest + +from notifications_utils.international_billing_rates import ( + COUNTRY_PREFIXES, + INTERNATIONAL_BILLING_RATES, +) +from notifications_utils.recipients import use_numeric_sender + + +def test_international_billing_rates_exists(): + assert INTERNATIONAL_BILLING_RATES["1"]["names"][0] == "Canada" + + +@pytest.mark.parametrize( + "country_prefix, values", sorted(INTERNATIONAL_BILLING_RATES.items()) +) +def test_international_billing_rates_are_in_correct_format(country_prefix, values): + assert isinstance(country_prefix, str) + # we don't want the prefixes to have + at the beginning for instance + assert country_prefix.isdigit() + + assert set(values.keys()) == {"attributes", "billable_units", "names"} + + assert isinstance(values["billable_units"], int) + assert 1 <= values["billable_units"] <= 3 + + assert isinstance(values["names"], list) + assert all(isinstance(country, str) for country in values["names"]) + + assert isinstance(values["attributes"], dict) + assert values["attributes"]["dlr"] is None or isinstance( + values["attributes"]["dlr"], str + ) + + +def test_country_codes(): + assert len(COUNTRY_PREFIXES) == 214 + + +@pytest.mark.parametrize( + "number, expected", + [ + ("+48123654789", False), # Poland alpha: Yes + ("+1-403-123-5687", True), # Canada alpha: No + ("+40123548897", False), # Romania alpha: REG + ("+60123451345", True), + ], +) # Malaysia alpha: NO +def test_use_numeric_sender(number, expected): + assert use_numeric_sender(number) == expected diff --git a/tests/notification_utils/test_letter_timings.py b/tests/notification_utils/test_letter_timings.py new file mode 100644 index 000000000..2a90fc641 --- /dev/null +++ b/tests/notification_utils/test_letter_timings.py @@ -0,0 +1,269 @@ +from datetime import datetime + +import pytest +import pytz +from freezegun import freeze_time + +from notifications_utils.letter_timings import ( + get_letter_timings, + letter_can_be_cancelled, +) + + +@freeze_time("2017-07-14 13:59:59") # Friday, before print deadline (3PM EST) +@pytest.mark.parametrize( + "upload_time, expected_print_time, is_printed, first_class, expected_earliest, expected_latest", + [ + # EST + # ================================================================== + # First thing Monday + ( + "Monday 2017-07-10 00:00:01", + "Tuesday 2017-07-11 15:00", + True, + "Wednesday 2017-07-12 16:00", + "Thursday 2017-07-13 16:00", + "Friday 2017-07-14 16:00", + ), + # Monday at 17:29 EST (sent on monday) + ( + "Monday 2017-07-10 16:29:59", + "Tuesday 2017-07-11 15:00", + True, + "Wednesday 2017-07-12 16:00", + "Thursday 2017-07-13 16:00", + "Friday 2017-07-14 16:00", + ), + # Monday at 17:30 EST (sent on tuesday) + ( + "Monday 2017-07-10 16:30:01", + "Wednesday 2017-07-12 15:00", + True, + "Thursday 2017-07-13 16:00", + "Friday 2017-07-14 16:00", + "Saturday 2017-07-15 16:00", + ), + # Tuesday before 17:30 EST + ( + "Tuesday 2017-07-11 12:00:00", + "Wednesday 2017-07-12 15:00", + True, + "Thursday 2017-07-13 16:00", + "Friday 2017-07-14 16:00", + "Saturday 2017-07-15 16:00", + ), + # Wednesday before 17:30 EST + ( + "Wednesday 2017-07-12 12:00:00", + "Thursday 2017-07-13 15:00", + True, + "Friday 2017-07-14 16:00", + "Saturday 2017-07-15 16:00", + "Monday 2017-07-17 16:00", + ), + # Thursday before 17:30 EST + ( + "Thursday 2017-07-13 12:00:00", + "Friday 2017-07-14 15:00", + False, + "Saturday 2017-07-15 16:00", + "Monday 2017-07-17 16:00", + "Tuesday 2017-07-18 16:00", + ), + # Friday anytime + ( + "Friday 2017-07-14 00:00:00", + "Monday 2017-07-17 15:00", + False, + "Tuesday 2017-07-18 16:00", + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + ), + ( + "Friday 2017-07-14 12:00:00", + "Monday 2017-07-17 15:00", + False, + "Tuesday 2017-07-18 16:00", + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + ), + ( + "Friday 2017-07-14 22:00:00", + "Monday 2017-07-17 15:00", + False, + "Tuesday 2017-07-18 16:00", + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + ), + # Saturday anytime + ( + "Saturday 2017-07-14 12:00:00", + "Monday 2017-07-17 15:00", + False, + "Tuesday 2017-07-18 16:00", + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + ), + # Sunday before 1730 EST + ( + "Sunday 2017-07-15 15:59:59", + "Monday 2017-07-17 15:00", + False, + "Tuesday 2017-07-18 16:00", + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + ), + # Sunday after 17:30 EST + ( + "Sunday 2017-07-16 16:30:01", + "Tuesday 2017-07-18 15:00", + False, + "Wednesday 2017-07-19 16:00", + "Thursday 2017-07-20 16:00", + "Friday 2017-07-21 16:00", + ), + # GMT + # ================================================================== + # Monday at 17:29 GMT + ( + "Monday 2017-01-02 17:29:59", + "Tuesday 2017-01-03 15:00", + True, + "Wednesday 2017-01-04 16:00", + "Thursday 2017-01-05 16:00", + "Friday 2017-01-06 16:00", + ), + # Monday at 17:00 GMT + ( + "Monday 2017-01-02 17:30:01", + "Wednesday 2017-01-04 15:00", + True, + "Thursday 2017-01-05 16:00", + "Friday 2017-01-06 16:00", + "Saturday 2017-01-07 16:00", + ), + ], +) +@pytest.mark.skip(reason="Letters being developed later") +def test_get_estimated_delivery_date_for_letter( + upload_time, + expected_print_time, + is_printed, + first_class, + expected_earliest, + expected_latest, +): + # remove the day string from the upload_time, which is purely informational + + def format_dt(x): + return x.astimezone(pytz.timezone("America/New_York")).strftime( + "%A %Y-%m-%d %H:%M" + ) + + upload_time = upload_time.split(" ", 1)[1] + + timings = get_letter_timings(upload_time, postage="second") + + assert format_dt(timings.printed_by) == expected_print_time + assert timings.is_printed == is_printed + assert format_dt(timings.earliest_delivery) == expected_earliest + assert format_dt(timings.latest_delivery) == expected_latest + + first_class_timings = get_letter_timings(upload_time, postage="first") + + assert format_dt(first_class_timings.printed_by) == expected_print_time + assert first_class_timings.is_printed == is_printed + assert format_dt(first_class_timings.earliest_delivery) == first_class + assert format_dt(first_class_timings.latest_delivery) == first_class + + +@pytest.mark.parametrize("status", ["sending", "pending"]) +def test_letter_cannot_be_cancelled_if_letter_status_is_not_created_or_pending_virus_check( + status, +): + notification_created_at = datetime.utcnow() + + assert not letter_can_be_cancelled(status, notification_created_at) + + +@freeze_time("2018-7-7 16:00:00") +@pytest.mark.parametrize( + "notification_created_at", + [ + datetime(2018, 7, 6, 18, 0), # created yesterday after 1730 + datetime(2018, 7, 7, 12, 0), # created today + ], +) +@pytest.mark.skip(reason="Letters not part of release") +def test_letter_can_be_cancelled_if_before_1730_and_letter_created_before_1730( + notification_created_at, +): + notification_status = "pending-virus-check" + + assert letter_can_be_cancelled(notification_status, notification_created_at) + + +@freeze_time("2017-12-12 17:30:00") +@pytest.mark.parametrize( + "notification_created_at", + [ + datetime(2017, 12, 12, 17, 0), + datetime(2017, 12, 12, 17, 30), + ], +) +@pytest.mark.skip(reason="Letters not part of release") +def test_letter_cannot_be_cancelled_if_1730_exactly_and_letter_created_at_or_before_1730( + notification_created_at, +): + notification_status = "pending-virus-check" + + assert not letter_can_be_cancelled(notification_status, notification_created_at) + + +@freeze_time("2018-7-7 19:00:00") +@pytest.mark.parametrize( + "notification_created_at", + [ + datetime(2018, 7, 6, 18, 0), # created yesterday after 1730 + datetime(2018, 7, 7, 12, 0), # created today before 1730 + ], +) +@pytest.mark.skip(reason="Letters not part of release") +def test_letter_cannot_be_cancelled_if_after_1730_and_letter_created_before_1730( + notification_created_at, +): + notification_status = "created" + + assert not letter_can_be_cancelled(notification_status, notification_created_at) + + +@freeze_time("2018-7-7 15:00:00") +@pytest.mark.skip(reason="Letters not part of release") +def test_letter_cannot_be_cancelled_if_before_1730_and_letter_created_before_1730_yesterday(): + notification_status = "created" + + assert not letter_can_be_cancelled(notification_status, datetime(2018, 7, 6, 14, 0)) + + +@freeze_time("2018-7-7 15:00:00") +@pytest.mark.skip(reason="Letters not part of release") +def test_letter_cannot_be_cancelled_if_before_1730_and_letter_created_after_1730_two_days_ago(): + notification_status = "created" + + assert not letter_can_be_cancelled(notification_status, datetime(2018, 7, 5, 19, 0)) + + +@freeze_time("2018-7-7 19:00:00") +@pytest.mark.parametrize( + "notification_created_at", + [ + datetime(2018, 7, 7, 18, 30), + datetime(2018, 7, 7, 19, 0), + ], +) +def test_letter_can_be_cancelled_if_after_1730_and_letter_created_at_1730_today_or_later( + notification_created_at, +): + notification_status = "created" + + assert letter_can_be_cancelled(notification_status, notification_created_at) diff --git a/tests/notification_utils/test_logging.py b/tests/notification_utils/test_logging.py new file mode 100644 index 000000000..1aefb4065 --- /dev/null +++ b/tests/notification_utils/test_logging.py @@ -0,0 +1,51 @@ +import json +import logging as builtin_logging + +from notifications_utils import logging + + +def test_get_handlers_sets_up_logging_appropriately_with_debug(): + class App: + config = {"NOTIFY_APP_NAME": "bar", "NOTIFY_LOG_LEVEL": "ERROR"} + debug = True + + app = App() + + handlers = logging.get_handlers(app) + + assert len(handlers) == 1 + assert isinstance(handlers[0], builtin_logging.StreamHandler) + assert isinstance(handlers[0].formatter, builtin_logging.Formatter) + + +def test_get_handlers_sets_up_logging_appropriately_without_debug(): + class App: + config = {"NOTIFY_APP_NAME": "bar", "NOTIFY_LOG_LEVEL": "ERROR"} + debug = False + + app = App() + + handlers = logging.get_handlers(app) + + assert len(handlers) == 1 + assert isinstance(handlers[0], builtin_logging.StreamHandler) + assert isinstance(handlers[0].formatter, logging.JSONFormatter) + + +def test_base_json_formatter_contains_service_id(): + record = builtin_logging.LogRecord( + name="log thing", + level="info", + pathname="path", + lineno=123, + msg="message to log", + exc_info=None, + args=None, + ) + + service_id_filter = logging.ServiceIdFilter() + assert ( + json.loads(logging.BaseJSONFormatter().format(record))["message"] + == "message to log" + ) + assert service_id_filter.filter(record).service_id == "notify-admin" diff --git a/tests/notification_utils/test_markdown.py b/tests/notification_utils/test_markdown.py new file mode 100644 index 000000000..550f56b58 --- /dev/null +++ b/tests/notification_utils/test_markdown.py @@ -0,0 +1,667 @@ +import pytest + +from notifications_utils.markdown import ( + notify_email_markdown, + notify_letter_preview_markdown, + notify_plain_text_email_markdown, +) +from notifications_utils.template import HTMLEmailTemplate + + +@pytest.mark.parametrize( + "url", + [ + "http://example.com", + "http://www.gov.uk/", + "https://www.gov.uk/", + "http://service.gov.uk", + "http://service.gov.uk/blah.ext?q=a%20b%20c&order=desc#fragment", + pytest.param( + "http://service.gov.uk/blah.ext?q=one two three", marks=pytest.mark.xfail + ), + ], +) +def test_makes_links_out_of_URLs(url): + link = '{}'.format( + url, url + ) + assert notify_email_markdown(url) == ( + '

    ' + "{}" + "

    " + ).format(link) + + +@pytest.mark.parametrize( + "input, output", + [ + ( + ("this is some text with a link http://example.com in the middle"), + ( + "this is some text with a link " + 'http://example.com' + " in the middle" + ), + ), + ( + ("this link is in brackets (http://example.com)"), + ( + "this link is in brackets " + '(http://example.com)' + ), + ), + ], +) +def test_makes_links_out_of_URLs_in_context(input, output): + assert notify_email_markdown(input) == ( + '

    ' + "{}" + "

    " + ).format(output) + + +@pytest.mark.parametrize( + "url", + [ + "example.com", + "www.example.com", + "ftp://example.com", + "test@example.com", + "mailto:test@example.com", + 'Example', + ], +) +def test_doesnt_make_links_out_of_invalid_urls(url): + assert notify_email_markdown(url) == ( + '

    ' + "{}" + "

    " + ).format(url) + + +def test_handles_placeholders_in_urls(): + assert notify_email_markdown( + "http://example.com/?token=((token))&key=1" + ) == ( + '

    ' + '' + "http://example.com/?token=" + "" + "((token))&key=1" + "

    " + ) + + +@pytest.mark.parametrize( + "url, expected_html, expected_html_in_template", + [ + ( + """https://example.com"onclick="alert('hi')""", + """https://example.com"onclick="alert('hi')""", # noqa + """https://example.com"onclick="alert('hi‘)""", # noqa + ), + ( + """https://example.com"style='text-decoration:blink'""", + """https://example.com"style='text-decoration:blink'""", # noqa + """https://example.com"style='text-decoration:blink’""", # noqa + ), + ], +) +def test_URLs_get_escaped(url, expected_html, expected_html_in_template): + assert notify_email_markdown(url) == ( + '

    ' + "{}" + "

    " + ).format(expected_html) + assert expected_html_in_template in str( + HTMLEmailTemplate( + { + "content": url, + "subject": "", + "template_type": "email", + } + ) + ) + + +@pytest.mark.parametrize( + "markdown_function, expected_output", + [ + ( + notify_email_markdown, + ( + '

    ' + '' + "https://example.com" + "" + "

    " + '

    ' + "Next paragraph" + "

    " + ), + ), + ( + notify_plain_text_email_markdown, + ("\n" "\nhttps://example.com" "\n" "\nNext paragraph"), + ), + ], +) +def test_preserves_whitespace_when_making_links(markdown_function, expected_output): + assert ( + markdown_function("https://example.com\n" "\n" "Next paragraph") + == expected_output + ) + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, 'print("hello")'], + [notify_email_markdown, 'print("hello")'], + [notify_plain_text_email_markdown, 'print("hello")'], + ), +) +def test_block_code(markdown_function, expected): + assert markdown_function('```\nprint("hello")\n```') == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, ("

    inset text

    ")], + [ + notify_email_markdown, + ( + "
    ' + '

    inset text

    ' + "
    " + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\ninset text"), + ], + ), +) +def test_block_quote(markdown_function, expected): + assert markdown_function("^ inset text") == expected + + +@pytest.mark.parametrize( + "heading", + ( + "# heading", + "#heading", + ), +) +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, "

    heading

    \n"], + [ + notify_email_markdown, + ( + '

    ' + "heading" + "

    " + ), + ], + [ + notify_plain_text_email_markdown, + ( + "\n" + "\n" + "\nheading" + "\n-----------------------------------------------------------------" + ), + ], + ), +) +def test_level_1_header(markdown_function, heading, expected): + assert markdown_function(heading) == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, "

    inset text

    "], + [ + notify_email_markdown, + '

    inset text

    ', + ], + [ + notify_plain_text_email_markdown, + ("\n" "\ninset text"), + ], + ), +) +def test_level_2_header(markdown_function, expected): + assert markdown_function("## inset text") == (expected) + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("

    a

    " '
     
    ' "

    b

    "), + ], + [ + notify_email_markdown, + ( + '

    a

    ' + '
    ' + '

    b

    ' + ), + ], + [ + notify_plain_text_email_markdown, + ( + "\n" + "\na" + "\n" + "\n=================================================================" + "\n" + "\nb" + ), + ], + ), +) +def test_hrule(markdown_function, expected): + assert markdown_function("a\n\n***\n\nb") == expected + assert markdown_function("a\n\n---\n\nb") == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("
      \n" "
    1. one
    2. \n" "
    3. two
    4. \n" "
    5. three
    6. \n" "
    \n"), + ], + [ + notify_email_markdown, + ( + '' + "" + '" + "" + "
    ' + '
      ' + '
    1. one
    2. ' + '
    3. two
    4. ' + '
    5. three
    6. ' + "
    " + "
    " + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\n1. one" "\n2. two" "\n3. three"), + ], + ), +) +def test_ordered_list(markdown_function, expected): + assert markdown_function("1. one\n" "2. two\n" "3. three\n") == expected + assert markdown_function("1.one\n" "2.two\n" "3.three\n") == expected + + +@pytest.mark.parametrize( + "markdown", + ( + ("*one\n" "*two\n" "*three\n"), # no space + ("* one\n" "* two\n" "* three\n"), # single space + ("* one\n" "* two\n" "* three\n"), # two spaces + ("* one\n" "* two\n" "* three\n"), # tab + ("- one\n" "- two\n" "- three\n"), # dash as bullet + pytest.param( + ("+ one\n" "+ two\n" "+ three\n"), # plus as bullet + marks=pytest.mark.xfail(raises=AssertionError), + ), + ("â€ĸ one\n" "â€ĸ two\n" "â€ĸ three\n"), # bullet as bullet + ), +) +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("
      \n" "
    • one
    • \n" "
    • two
    • \n" "
    • three
    • \n" "
    \n"), + ], + [ + notify_email_markdown, + ( + '' + "" + '" + "" + "
    ' + '
      ' + '
    • one
    • ' + '
    • two
    • ' + '
    • three
    • ' + "
    " + "
    " + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\nâ€ĸ one" "\nâ€ĸ two" "\nâ€ĸ three"), + ], + ), +) +def test_unordered_list(markdown, markdown_function, expected): + assert markdown_function(markdown) == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + "

    + one

    + two

    + three

    ", + ], + [ + notify_email_markdown, + ( + '

    + one

    ' + '

    + two

    ' + '

    + three

    ' + ), + ], + [ + notify_plain_text_email_markdown, + ("\n\n+ one" "\n\n+ two" "\n\n+ three"), + ], + ), +) +def test_pluses_dont_render_as_lists(markdown_function, expected): + assert markdown_function("+ one\n" "+ two\n" "+ three\n") == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("

    " "line one
    " "line two" "

    " "

    " "new paragraph" "

    "), + ], + [ + notify_email_markdown, + ( + '

    line one
    ' + "line two

    " + '

    new paragraph

    ' + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\nline one" "\nline two" "\n" "\nnew paragraph"), + ], + ), +) +def test_paragraphs(markdown_function, expected): + assert markdown_function("line one\n" "line two\n" "\n" "new paragraph") == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, ("

    before

    " "

    after

    ")], + [ + notify_email_markdown, + ( + '

    before

    ' + '

    after

    ' + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\nbefore" "\n" "\nafter"), + ], + ), +) +def test_multiple_newlines_get_truncated(markdown_function, expected): + assert markdown_function("before\n\n\n\n\n\nafter") == expected + + +@pytest.mark.parametrize( + "markdown_function", + ( + notify_letter_preview_markdown, + notify_email_markdown, + notify_plain_text_email_markdown, + ), +) +def test_table(markdown_function): + assert markdown_function("col | col\n" "----|----\n" "val | val\n") == ("") + + +@pytest.mark.parametrize( + "markdown_function, link, expected", + ( + [ + notify_letter_preview_markdown, + "http://example.com", + "

    example.com

    ", + ], + [ + notify_email_markdown, + "http://example.com", + ( + '

    ' + 'http://example.com' + "

    " + ), + ], + [ + notify_email_markdown, + """https://example.com"onclick="alert('hi')""", + ( + '

    ' + '' + 'https://example.com"onclick="alert(\'hi' + "')" + "

    " + ), + ], + [ + notify_plain_text_email_markdown, + "http://example.com", + ("\n" "\nhttp://example.com"), + ], + ), +) +def test_autolink(markdown_function, link, expected): + assert markdown_function(link) == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, "

    variable called `thing`

    "], + [ + notify_email_markdown, + '

    variable called `thing`

    ', # noqa E501 + ], + [ + notify_plain_text_email_markdown, + "\n\nvariable called `thing`", + ], + ), +) +def test_codespan(markdown_function, expected): + assert markdown_function("variable called `thing`") == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, "

    something **important**

    "], + [ + notify_email_markdown, + '

    something **important**

    ', # noqa E501 + ], + [ + notify_plain_text_email_markdown, + "\n\nsomething **important**", + ], + ), +) +def test_double_emphasis(markdown_function, expected): + assert markdown_function("something **important**") == expected + + +@pytest.mark.parametrize( + "markdown_function, text, expected", + ( + [ + notify_letter_preview_markdown, + "something *important*", + "

    something *important*

    ", + ], + [ + notify_email_markdown, + "something *important*", + '

    something *important*

    ', # noqa E501 + ], + [ + notify_plain_text_email_markdown, + "something *important*", + "\n\nsomething *important*", + ], + [ + notify_plain_text_email_markdown, + "something _important_", + "\n\nsomething _important_", + ], + [ + notify_plain_text_email_markdown, + "before*after", + "\n\nbefore*after", + ], + [ + notify_plain_text_email_markdown, + "before_after", + "\n\nbefore_after", + ], + ), +) +def test_emphasis(markdown_function, text, expected): + assert markdown_function(text) == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_email_markdown, + '

    foo ****** bar

    ', + ], + [ + notify_plain_text_email_markdown, + "\n\nfoo ****** bar", + ], + ), +) +def test_nested_emphasis(markdown_function, expected): + assert markdown_function("foo ****** bar") == expected + + +@pytest.mark.parametrize( + "markdown_function", + ( + notify_letter_preview_markdown, + notify_email_markdown, + notify_plain_text_email_markdown, + ), +) +def test_image(markdown_function): + assert markdown_function("![alt text](http://example.com/image.png)") == ("") + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("

    Example: example.com

    "), + ], + [ + notify_email_markdown, + ( + '

    ' + 'Example' + "

    " + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\nExample: http://example.com"), + ], + ), +) +def test_link(markdown_function, expected): + assert markdown_function("[Example](http://example.com)") == expected + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [ + notify_letter_preview_markdown, + ("

    Example: example.com

    "), + ], + [ + notify_email_markdown, + ( + '

    ' + '' + "Example" + "" + "

    " + ), + ], + [ + notify_plain_text_email_markdown, + ("\n" "\nExample (An example URL): http://example.com"), + ], + ), +) +def test_link_with_title(markdown_function, expected): + assert ( + markdown_function('[Example](http://example.com "An example URL")') == expected + ) + + +@pytest.mark.parametrize( + "markdown_function, expected", + ( + [notify_letter_preview_markdown, "

    ~~Strike~~

    "], + [ + notify_email_markdown, + '

    ~~Strike~~

    ', + ], + [notify_plain_text_email_markdown, "\n\n~~Strike~~"], + ), +) +def test_strikethrough(markdown_function, expected): + assert markdown_function("~~Strike~~") == expected + + +def test_footnotes(): + # Can’t work out how to test this + pass diff --git a/tests/notification_utils/test_placeholders.py b/tests/notification_utils/test_placeholders.py new file mode 100644 index 000000000..7ac11c3c9 --- /dev/null +++ b/tests/notification_utils/test_placeholders.py @@ -0,0 +1,66 @@ +import re + +import pytest + +from notifications_utils.field import Placeholder + + +@pytest.mark.parametrize( + "body, expected", + [ + ("((with-brackets))", "with-brackets"), + ("without-brackets", "without-brackets"), + ], +) +def test_placeholder_returns_name(body, expected): + assert Placeholder(body).name == expected + + +@pytest.mark.parametrize( + "body, is_conditional", + [ + ("not a conditional", False), + ("not? a conditional", False), + ("a?? conditional", True), + ], +) +def test_placeholder_identifies_conditional(body, is_conditional): + assert Placeholder(body).is_conditional() == is_conditional + + +@pytest.mark.parametrize( + "body, conditional_text", + [ + ("a??b", "b"), + ("a?? b ", " b "), + ("a??b??c", "b??c"), + ], +) +def test_placeholder_gets_conditional_text(body, conditional_text): + assert Placeholder(body).conditional_text == conditional_text + + +def test_placeholder_raises_if_accessing_conditional_text_on_non_conditional(): + with pytest.raises(ValueError): + Placeholder("hello").conditional_text + + +@pytest.mark.parametrize( + "body, value, result", + [ + ("a??b", "Yes", "b"), + ("a??b", "No", ""), + ], +) +def test_placeholder_gets_conditional_body(body, value, result): + assert Placeholder(body).get_conditional_body(value) == result + + +def test_placeholder_raises_if_getting_conditional_body_on_non_conditional(): + with pytest.raises(ValueError): + Placeholder("hello").get_conditional_body("Yes") + + +def test_placeholder_can_be_constructed_from_regex_match(): + match = re.search(r"\(\(.*\)\)", "foo ((bar)) baz") + assert Placeholder.from_match(match).name == "bar" diff --git a/tests/notification_utils/test_postal_address.py b/tests/notification_utils/test_postal_address.py new file mode 100644 index 000000000..f854944cd --- /dev/null +++ b/tests/notification_utils/test_postal_address.py @@ -0,0 +1,777 @@ +import pytest + +from notifications_utils.countries import Country +from notifications_utils.countries.data import Postage +from notifications_utils.insensitive_dict import InsensitiveDict +from notifications_utils.postal_address import ( + PostalAddress, + format_postcode_for_printing, + is_a_real_uk_postcode, + normalise_postcode, +) + + +def test_raw_address(): + raw_address = "a\n\n\tb\r c " + assert PostalAddress(raw_address).raw_address == raw_address + + +@pytest.mark.parametrize( + "address, expected_country", + ( + ( + """ + 123 Example Street + City of Town + SW1A 1AA + """, + Country("United Kingdom"), + ), + ( + """ + 123 Example Street + City of Town + SW1A 1AA + United Kingdom + """, + Country("United Kingdom"), + ), + ( + """ + 123 Example Street + City of Town + Wales + """, + Country("United Kingdom"), + ), + ( + """ + 123 Example Straße + Deutschland + """, + Country("Germany"), + ), + ), +) +def test_country(address, expected_country): + assert PostalAddress(address).country == expected_country + + +@pytest.mark.parametrize( + "address, enough_lines_expected", + ( + ( + "", + False, + ), + ( + """ + 123 Example Street + City of Town + SW1A 1AA + """, + True, + ), + ( + """ + 123 Example Street + City of Town + United Kingdom + """, + False, + ), + ( + """ + 123 Example Street + + + City of Town + """, + False, + ), + ( + """ + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + """, + True, + ), + ), +) +def test_has_enough_lines(address, enough_lines_expected): + assert PostalAddress(address).has_enough_lines is enough_lines_expected + + +@pytest.mark.parametrize( + "address, too_many_lines_expected", + ( + ( + "", + False, + ), + ( + """ + Line 1 + Line 2 + Line 3 + Line 4 + Line 5 + Line 6 + Line 7 + """, + False, + ), + ( + """ + Line 1 + + Line 2 + + Line 3 + + Line 4 + + Line 5 + + Line 6 + + Line 7 + """, + False, + ), + ( + """ + Line 1 + Line 2 + Line 3 + Line 4 + Line 5 + Line 6 + Line 7 + Scotland + """, + False, + ), + ( + """ + Line 1 + Line 2 + Line 3 + Line 4 + Line 5 + Line 6 + Line 7 + Line 8 + """, + True, + ), + ), +) +def test_has_too_many_lines(address, too_many_lines_expected): + assert PostalAddress(address).has_too_many_lines is too_many_lines_expected + + +@pytest.mark.parametrize( + "address, expected_postcode", + ( + ( + "", + None, + ), + ( + """ + 123 Example Street + City of Town + SW1A 1AA + """, + "SW1A 1AA", + ), + ( + """ + 123 Example Street + City of Town + S W1 A 1 AA + """, + "SW1A 1AA", + ), + ( + """ + 123 Example Straße + Deutschland + """, + None, + ), + ( + """ + 123 Example Straße + SW1A 1AA + Deutschland + """, + None, + ), + ), +) +def test_postcode(address, expected_postcode): + assert PostalAddress(address).has_valid_postcode is bool(expected_postcode) + assert PostalAddress(address).postcode == expected_postcode + + +@pytest.mark.parametrize( + "address, expected_result", + [ + ( + "", + False, + ), + ( + """ + 1[23 Example Street) + C@ity of Town + SW1A 1AA + """, + False, + ), + ( + """ + [123 Example Street + (ity of Town + ]S W1 A 1 AA + """, + True, + ), + ( + r""" + 123 Example Straße + SW1A 1AA + \Deutschland + """, + True, + ), + ( + r""" + >123 Example Straße + SW1A 1AA + Deutschland + """, + True, + ), + ( + """ + ~123 Example Street + City of Town + SW1 A 1 AA + """, + True, + ), + ], +) +def test_has_invalid_characters(address, expected_result): + assert PostalAddress(address).has_invalid_characters is expected_result + + +@pytest.mark.parametrize( + "address, expected_international", + ( + ( + "", + False, + ), + ( + """ + 123 Example Street + City of Town + SW1A 1AA + """, + False, + ), + ( + """ + 123 Example Street + City of Town + United Kingdom + """, + False, + ), + ( + """ + 123 Example Street + City of Town + Guernsey + """, + False, + ), + ( + """ + 123 Example Straße + Deutschland + """, + True, + ), + ), +) +def test_international(address, expected_international): + assert PostalAddress(address).international is expected_international + + +@pytest.mark.parametrize( + "address, expected_normalised, expected_as_single_line", + ( + ( + "", + "", + "", + ), + ( + """ + 123 Example St . + City of Town + + S W1 A 1 AA + """, + ("123 Example St.\n" "City of Town\n" "SW1A 1AA"), + ("123 Example St., City of Town, SW1A 1AA"), + ), + ( + ( + "123 Example St. \t , \n" + ", , , , , , ,\n" + "City of Town, Region,\n" + "SW1A 1AA,,\n" + ), + ("123 Example St.\n" "City of Town, Region\n" "SW1A 1AA"), + ("123 Example St., City of Town, Region, SW1A 1AA"), + ), + ( + """ + 123 Example Straße + Deutschland + + + """, + ("123 Example Straße\n" "Germany"), + ("123 Example Straße, Germany"), + ), + ), +) +def test_normalised(address, expected_normalised, expected_as_single_line): + assert PostalAddress(address).normalised == expected_normalised + assert PostalAddress(address).as_single_line == expected_as_single_line + + +@pytest.mark.parametrize( + "address, expected_postage", + ( + ( + "", + Postage.UK, + ), + ( + """ + 123 Example Street + City of Town + SW1A 1AA + """, + Postage.UK, + ), + ( + """ + 123 Example Street + City of Town + Scotland + """, + Postage.UK, + ), + ( + """ + 123 Example Straße + Deutschland + """, + Postage.EUROPE, + ), + ( + """ + 123 Rue Example + Côte d'Ivoire + """, + Postage.REST_OF_WORLD, + ), + ), +) +def test_postage(address, expected_postage): + assert PostalAddress(address).postage == expected_postage + + +@pytest.mark.parametrize( + "personalisation", + ( + { + "address_line_1": "123 Example Street", + "address_line_3": "City of Town", + "address_line_4": "", + "postcode": "SW1A1AA", + "ignore me": "ignore me", + }, + { + "address_line_1": "123 Example Street", + "address_line_3": "City of Town", + "address_line_4": "SW1A1AA", + }, + { + "address_line_2": "123 Example Street", + "address_line_5": "City of Town", + "address_line_7": "SW1A1AA", + }, + { + "address_line_1": "123 Example Street", + "address_line_3": "City of Town", + "address_line_7": "SW1A1AA", + "postcode": "ignored if address line 7 provided", + }, + InsensitiveDict( + { + "address line 1": "123 Example Street", + "ADDRESS_LINE_2": "City of Town", + "Address-Line-7": "Sw1a 1aa", + } + ), + ), +) +def test_from_personalisation(personalisation): + assert PostalAddress.from_personalisation(personalisation).normalised == ( + "123 Example Street\n" "City of Town\n" "SW1A 1AA" + ) + + +def test_from_personalisation_handles_int(): + personalisation = { + "address_line_1": 123, + "address_line_2": "Example Street", + "address_line_3": "City of Town", + "address_line_4": "SW1A1AA", + } + assert PostalAddress.from_personalisation(personalisation).normalised == ( + "123\n" "Example Street\n" "City of Town\n" "SW1A 1AA" + ) + + +@pytest.mark.parametrize( + "address, expected_personalisation", + ( + ( + "", + { + "address_line_1": "", + "address_line_2": "", + "address_line_3": "", + "address_line_4": "", + "address_line_5": "", + "address_line_6": "", + "address_line_7": "", + "postcode": "", + }, + ), + ( + """ + 123 Example Street + City of Town + SW1A1AA + """, + { + "address_line_1": "123 Example Street", + "address_line_2": "City of Town", + "address_line_3": "", + "address_line_4": "", + "address_line_5": "", + "address_line_6": "", + "address_line_7": "SW1A 1AA", + "postcode": "SW1A 1AA", + }, + ), + ( + """ + One + Two + Three + Four + Five + Six + Seven + Eight + """, + { + "address_line_1": "One", + "address_line_2": "Two", + "address_line_3": "Three", + "address_line_4": "Four", + "address_line_5": "Five", + "address_line_6": "Six", + "address_line_7": "Eight", + "postcode": "Eight", + }, + ), + ), +) +def test_as_personalisation(address, expected_personalisation): + assert PostalAddress(address).as_personalisation == expected_personalisation + + +@pytest.mark.parametrize( + "address, expected_bool", + ( + ("", False), + (" ", False), + ("\n\n \n", False), + ("a", True), + ), +) +def test_bool(address, expected_bool): + assert bool(PostalAddress(address)) is expected_bool + + +@pytest.mark.parametrize( + "postcode, normalised_postcode", + [ + ("SW1 3EF", "SW13EF"), + ("SW13EF", "SW13EF"), + ("sw13ef", "SW13EF"), + ("Sw13ef", "SW13EF"), + ("sw1 3ef", "SW13EF"), + (" SW1 3EF ", "SW13EF"), + ], +) +def test_normalise_postcode(postcode, normalised_postcode): + assert normalise_postcode(postcode) == normalised_postcode + + +@pytest.mark.parametrize( + "postcode, result", + [ + # real standard UK poscodes + ("SW1 3EF", True), + ("SW13EF", True), + ("SE1 63EF", True), + ("N5 1AA", True), + ("SO14 6WB", True), + ("so14 6wb", True), + ("so14\u00A06wb", True), + # invalida / incomplete postcodes + ("N5", False), + ("SO144 6WB", False), + ("SO14 6WBA", False), + ("", False), + ("Bad postcode", False), + # valid British Forces postcodes + ("BFPO1234", True), + ("BFPO C/O 1234", True), + ("BFPO 1234", True), + ("BFPO1", True), + # invalid British Forces postcodes + ("BFPO", False), + ("BFPO12345", False), + # Giro Bank valid postcode and invalid postcode + ("GIR0AA", True), + ("GIR0AB", False), + ], +) +def test_if_postcode_is_a_real_uk_postcode(postcode, result): + assert is_a_real_uk_postcode(postcode) is result + + +def test_if_postcode_is_a_real_uk_postcode_normalises_before_checking_postcode(mocker): + normalise_postcode_mock = mocker.patch( + "notifications_utils.postal_address.normalise_postcode" + ) + normalise_postcode_mock.return_value = "SW11AA" + assert is_a_real_uk_postcode("sw1 1aa") is True + + +@pytest.mark.parametrize( + "postcode, postcode_with_space", + [ + ("SW13EF", "SW1 3EF"), + ("SW1 3EF", "SW1 3EF"), + ("N5 3EF", "N5 3EF"), + ("N5 3EF", "N5 3EF"), + ("N53EF ", "N5 3EF"), + ("n53Ef", "N5 3EF"), + ("n5 \u00A0 \t 3Ef", "N5 3EF"), + ("SO146WB", "SO14 6WB"), + ("BFPO2", "BFPO 2"), + ("BFPO232", "BFPO 232"), + ("BFPO 2432", "BFPO 2432"), + ("BFPO C/O 2", "BFPO C/O 2"), + ("BFPO c/o 232", "BFPO C/O 232"), + ("GIR0AA", "GIR 0AA"), + ], +) +def test_format_postcode_for_printing(postcode, postcode_with_space): + assert format_postcode_for_printing(postcode) == postcode_with_space + + +@pytest.mark.parametrize( + "address, international, expected_valid", + ( + ( + """ + UK address + Service can’t send internationally + SW1A 1AA + """, + False, + True, + ), + ( + """ + UK address + Service can send internationally + SW1A 1AA + """, + True, + True, + ), + ( + """ + Overseas address + Service can’t send internationally + Guinea-Bissau + """, + False, + False, + ), + ( + """ + Overseas address + Service can send internationally + Guinea-Bissau + """, + True, + True, + ), + ( + """ + Overly long address + 2 + 3 + 4 + 5 + 6 + 7 + 8 + """, + True, + False, + ), + ( + """ + Address too short + 2 + """, + True, + False, + ), + ( + """ + No postcode or country + Service can’t send internationally + 3 + """, + False, + False, + ), + ( + """ + No postcode or country + Service can send internationally + 3 + """, + True, + False, + ), + ( + """ + Postcode and country + Service can’t send internationally + SW1 1AA + France + """, + False, + False, + ), + ), +) +def test_valid_with_international_parameter(address, international, expected_valid): + postal_address = PostalAddress( + address, + allow_international_letters=international, + ) + assert postal_address.valid is expected_valid + assert postal_address.has_valid_last_line is expected_valid + + +@pytest.mark.parametrize( + "address", + ( + """ + Too short, valid postcode + SW1A 1AA + """, + """ + Too short, valid country + Bhutan + """, + """ + Too long, valid postcode + 2 + 3 + 4 + 5 + 6 + 7 + SW1A 1AA + """, + """ + Too long, valid country + 2 + 3 + 4 + 5 + 6 + 7 + Bhutan + """, + ), +) +def test_valid_last_line_too_short_too_long(address): + postal_address = PostalAddress(address, allow_international_letters=True) + assert postal_address.valid is False + assert postal_address.has_valid_last_line is True + + +def test_valid_with_invalid_characters(): + address = "Valid\nExcept\n[For one character\nBhutan\n" + assert PostalAddress(address, allow_international_letters=True).valid is False + + +@pytest.mark.parametrize( + "international, expected_valid", + ( + (False, False), + (True, True), + ), +) +def test_valid_from_personalisation_with_international_parameter( + international, expected_valid +): + assert ( + PostalAddress.from_personalisation( + {"address_line_1": "A", "address_line_2": "B", "address_line_3": "Chad"}, + allow_international_letters=international, + ).valid + is expected_valid + ) diff --git a/tests/notification_utils/test_recipient_csv.py b/tests/notification_utils/test_recipient_csv.py new file mode 100644 index 000000000..e11bb14ad --- /dev/null +++ b/tests/notification_utils/test_recipient_csv.py @@ -0,0 +1,1356 @@ +import itertools +import string +import unicodedata +from functools import partial +from random import choice, randrange +from unittest.mock import Mock + +import pytest +from ordered_set import OrderedSet + +from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.countries import Country +from notifications_utils.formatters import strip_and_remove_obscure_whitespace +from notifications_utils.recipients import ( + Cell, + RecipientCSV, + Row, + first_column_headings, +) +from notifications_utils.template import ( + EmailPreviewTemplate, + LetterImageTemplate, + SMSMessageTemplate, +) + + +def _sample_template(template_type, content="foo"): + return { + "email": EmailPreviewTemplate( + {"content": content, "subject": "bar", "template_type": "email"} + ), + "sms": SMSMessageTemplate({"content": content, "template_type": "sms"}), + "letter": LetterImageTemplate( + {"content": content, "subject": "bar", "template_type": "letter"}, + image_url="https://example.com", + page_count=1, + ), + }.get(template_type) + + +def _index_rows(rows): + return set(row.index for row in rows) + + +@pytest.mark.parametrize( + "template_type, expected", + ( + ("email", ["email address"]), + ("sms", ["phone number"]), + ( + "letter", + [ + "address line 1", + "address line 2", + "address line 3", + "address line 4", + "address line 5", + "address line 6", + "postcode", + "address line 7", + ], + ), + ), +) +def test_recipient_column_headers(template_type, expected): + recipients = RecipientCSV("", template=_sample_template(template_type)) + assert ( + (recipients.recipient_column_headers) + == (first_column_headings[template_type]) + == (expected) + ) + + +@pytest.mark.parametrize( + "file_contents,template_type,expected", + [ + ( + "", + "sms", + [], + ), + ( + "phone number", + "sms", + [], + ), + ( + """ + phone number,name + +44 123, test1 + +44 456,test2 + """, + "sms", + [ + [("phone number", "+44 123"), ("name", "test1")], + [("phone number", "+44 456"), ("name", "test2")], + ], + ), + ( + """ + phone number,name + +44 123, + +44 456 + """, + "sms", + [ + [("phone number", "+44 123"), ("name", None)], + [("phone number", "+44 456"), ("name", None)], + ], + ), + ( + """ + email address,name + test@example.com,test1 + test2@example.com, test2 + """, + "email", + [ + [("email address", "test@example.com"), ("name", "test1")], + [("email address", "test2@example.com"), ("name", "test2")], + ], + ), + ( + """ + email address + test@example.com,test1,red + test2@example.com, test2,blue + """, + "email", + [ + [("email address", "test@example.com"), (None, ["test1", "red"])], + [("email address", "test2@example.com"), (None, ["test2", "blue"])], + ], + ), + ( + """ + email address,name + test@example.com,"test1" + test2@example.com," test2 " + test3@example.com," test3" + """, + "email", + [ + [("email address", "test@example.com"), ("name", "test1")], + [("email address", "test2@example.com"), ("name", "test2")], + [("email address", "test3@example.com"), ("name", "test3")], + ], + ), + ( + """ + email address,date,name + test@example.com,"Nov 28, 2016",test1 + test2@example.com,"Nov 29, 2016",test2 + """, + "email", + [ + [ + ("email address", "test@example.com"), + ("date", "Nov 28, 2016"), + ("name", "test1"), + ], + [ + ("email address", "test2@example.com"), + ("date", "Nov 29, 2016"), + ("name", "test2"), + ], + ], + ), + ( + """ + address_line_1 + Alice + Bob + """, + "letter", + [[("address_line_1", "Alice")], [("address_line_1", "Bob")]], + ), + ( + """ + address line 1,address line 2,address line 5,address line 6,postcode,name,thing + A. Name,,,,XM4 5HQ,example,example + """, + "letter", + [ + [ + ("addressline1", "A. Name"), + ("addressline2", None), + # optional address rows 3 and 4 not in file + ("addressline5", None), + ("addressline5", None), + ("postcode", "XM4 5HQ"), + ("name", "example"), + ("thing", "example"), + ] + ], + ), + ( + """ + phone number, list, list, list + 07900900001, cat, rat, gnat + 07900900002, dog, hog, frog + 07900900003, elephant + """, + "sms", + [ + [("phone number", "07900900001"), ("list", ["cat", "rat", "gnat"])], + [("phone number", "07900900002"), ("list", ["dog", "hog", "frog"])], + [("phone number", "07900900003"), ("list", ["elephant", None, None])], + ], + ), + ], +) +def test_get_rows(file_contents, template_type, expected): + rows = list( + RecipientCSV(file_contents, template=_sample_template(template_type)).rows + ) + if not expected: + assert rows == expected + for index, row in enumerate(expected): + assert len(rows[index].items()) == len(row) + for key, value in row: + assert rows[index].get(key).data == value + + +def test_get_rows_does_no_error_checking_of_rows_or_cells(mocker): + has_error_mock = mocker.patch.object(Row, "has_error") + has_bad_recipient_mock = mocker.patch.object(Row, "has_bad_recipient") + has_missing_data_mock = mocker.patch.object(Row, "has_missing_data") + cell_recipient_error_mock = mocker.patch.object(Cell, "recipient_error") + + recipients = RecipientCSV( + """ + email address, name + a@b.com, + a@b.com, My Name + a@b.com, + + + """, + template=_sample_template("email", "hello ((name))"), + max_errors_shown=3, + ) + + rows = recipients.get_rows() + for _ in range(3): + assert next(rows).recipient == "a@b.com" + + assert has_error_mock.called is False + assert has_bad_recipient_mock.called is False + assert has_missing_data_mock.called is False + assert cell_recipient_error_mock.called is False + + +def test_get_rows_only_iterates_over_file_once(mocker): + row_mock = mocker.patch("notifications_utils.recipients.Row") + + recipients = RecipientCSV( + """ + email address, name + a@b.com, + a@b.com, My Name + a@b.com, + + + """, + template=_sample_template("email", "hello ((name))"), + ) + + rows = recipients.get_rows() + for _ in range(3): + next(rows) + + assert row_mock.call_count == 3 + assert recipients.rows_as_list is None + + +@pytest.mark.parametrize( + "file_contents,template_type,expected", + [ + ( + """ + phone number,name + 2348675309, test1 + +1234-867-5301,test2 + , + """, + "sms", + [ + {"index": 0, "message_too_long": False}, + {"index": 1, "message_too_long": False}, + ], + ), + ( + """ + email address,name,colour + test@example.com,test1,blue + test2@example.com, test2,red + """, + "email", + [ + {"index": 0, "message_too_long": False}, + {"index": 1, "message_too_long": False}, + ], + ), + ], +) +def test_get_annotated_rows(file_contents, template_type, expected): + recipients = RecipientCSV( + file_contents, + template=_sample_template(template_type, "hello ((name))"), + max_initial_rows_shown=1, + ) + for index, expected_row in enumerate(expected): + annotated_row = list(recipients.rows)[index] + assert annotated_row.index == expected_row["index"] + assert annotated_row.message_too_long == expected_row["message_too_long"] + assert len(list(recipients.rows)) == 2 + assert len(list(recipients.initial_rows)) == 1 + assert not recipients.has_errors + + +def test_get_rows_with_errors(): + recipients = RecipientCSV( + """ + email address, name + a@b.com, + a@b.com, + a@b.com, + a@b.com, + a@b.com, + a@b.com, + + + """, + template=_sample_template("email", "hello ((name))"), + max_errors_shown=3, + ) + assert len(list(recipients.rows_with_errors)) == 6 + assert len(list(recipients.initial_rows_with_errors)) == 3 + assert recipients.has_errors + + +@pytest.mark.parametrize( + "template_type, row_count, header, filler, row_with_error", + [ + ( + "email", + 500, + "email address\n", + "test@example.com\n", + "test at example dot com", + ), + ("sms", 500, "phone number\n", "2348675309\n", "12345"), + ], +) +def test_big_list_validates_right_through( + template_type, row_count, header, filler, row_with_error +): + big_csv = RecipientCSV( + header + (filler * (row_count - 1) + row_with_error), + template=_sample_template(template_type), + max_errors_shown=100, + max_initial_rows_shown=3, + ) + assert len(list(big_csv.rows)) == row_count + assert _index_rows(big_csv.rows_with_bad_recipients) == {row_count - 1} # 0 indexed + assert _index_rows(big_csv.rows_with_errors) == {row_count - 1} + assert len(list(big_csv.initial_rows_with_errors)) == 1 + assert big_csv.has_errors + + +@pytest.mark.parametrize( + "template_type, row_count, header, filler", + [ + ("email", 50, "email address\n", "test@example.com\n"), + ("sms", 50, "phone number\n", "07900900123\n"), + ], +) +def test_check_if_message_too_long_for_sms_but_not_email_in_CSV( + mocker, template_type, row_count, header, filler +): + # we do not validate email size for CSVs to avoid performance issues + RecipientCSV( + header + filler * row_count, + template=_sample_template(template_type), + max_errors_shown=100, + max_initial_rows_shown=3, + ) + is_message_too_long = mocker.patch( + "notifications_utils.template.Template.is_message_too_long", side_effect=False + ) + if template_type == "email": + is_message_too_long.assert_not_called + else: + is_message_too_long.called + + +def test_overly_big_list_stops_processing_rows_beyond_max(mocker): + mock_strip_and_remove_obscure_whitespace = mocker.patch( + "notifications_utils.recipients.strip_and_remove_obscure_whitespace", + wraps=strip_and_remove_obscure_whitespace, + ) + mock_insert_or_append_to_dict = mocker.patch( + "notifications_utils.recipients.insert_or_append_to_dict" + ) + + big_csv = RecipientCSV( + "phonenumber,name\n" + ("2348675309,example\n" * 123), + template=_sample_template("sms", content="hello ((name))"), + ) + big_csv.max_rows = 10 + + # Our CSV has lots of rowsâ€Ļ + assert big_csv.too_many_rows + assert len(big_csv) == 123 + + # â€Ļbut we’ve only called the expensive whitespace function on each + # of the 2 cells in the first 10 rows + assert len(mock_strip_and_remove_obscure_whitespace.call_args_list) == 20 + + # â€Ļand we’ve only called the function which builds the internal data + # structure once for each of the first 10 rows + assert len(mock_insert_or_append_to_dict.call_args_list) == 10 + + +def test_file_with_lots_of_empty_columns(): + process = Mock() + + lots_of_commas = "," * 10_000 + + for row in RecipientCSV( + f"phone_number{lots_of_commas}\n" + (f"07900900900{lots_of_commas}\n" * 100), + template=_sample_template("sms"), + ): + assert [(key, cell.data) for key, cell in row.items()] == [ + # Note that we haven’t stored any of the empty cells + ("phonenumber", "07900900900") + ] + process() + + assert process.call_count == 100 + + +def test_empty_column_names(): + recipient_csv = RecipientCSV( + """ + phone_number,,,name + 07900900123,foo,bar,baz + """, + template=_sample_template("sms"), + ) + + assert recipient_csv[0]["phone_number"].data == "07900900123" + assert recipient_csv[0][""].data == ["foo", "bar"] + assert recipient_csv[0]["name"].data == "baz" + + +@pytest.mark.parametrize( + "file_contents,template,expected_recipients,expected_personalisation", + [ + ( + """ + phone number,name, date + +44 123,test1,today + +44456, ,tomorrow + ,, + , , + """, + _sample_template("sms", "hello ((name))"), + ["+44 123", "+44456"], + [{"name": "test1"}, {"name": None}], + ), + ( + """ + email address,name,colour + test@example.com,test1,red + testatexampledotcom,test2,blue + """, + _sample_template("email", "((colour))"), + ["test@example.com", "testatexampledotcom"], + [{"colour": "red"}, {"colour": "blue"}], + ), + ( + """ + email address + test@example.com,test1,red + testatexampledotcom,test2,blue + """, + _sample_template("email"), + ["test@example.com", "testatexampledotcom"], + [], + ), + ], +) +def test_get_recipient( + file_contents, template, expected_recipients, expected_personalisation +): + recipients = RecipientCSV(file_contents, template=template) + + for index, row in enumerate(expected_personalisation): + for key, value in row.items(): + assert recipients[index].recipient == expected_recipients[index] + assert recipients[index].personalisation.get(key) == value + + +@pytest.mark.parametrize( + "file_contents,template,expected_recipients,expected_personalisation", + [ + ( + """ + email address,test + test@example.com,test1,red + testatexampledotcom,test2,blue + """, + _sample_template("email", "((test))"), + [(0, "test@example.com"), (1, "testatexampledotcom")], + [ + {"emailaddress": "test@example.com", "test": "test1"}, + {"emailaddress": "testatexampledotcom", "test": "test2"}, + ], + ) + ], +) +def test_get_recipient_respects_order( + file_contents, template, expected_recipients, expected_personalisation +): + recipients = RecipientCSV(file_contents, template=template) + + for row, email in expected_recipients: + assert ( + recipients[row].index, + recipients[row].recipient, + recipients[row].personalisation, + ) == ( + row, + email, + expected_personalisation[row], + ) + + +@pytest.mark.parametrize( + "file_contents,template_type,expected,expected_missing", + [ + ("", "sms", [], set(["phone number", "name"])), + ( + """ + phone number,name + 2348675309,test1 + 2348675309,test1 + 2348675309,test1 + """, + "sms", + ["phone number", "name"], + set(), + ), + ( + """ + email address,name,colour + """, + "email", + ["email address", "name", "colour"], + set(), + ), + ( + """ + address_line_1, address_line_2, postcode, name + """, + "letter", + ["address_line_1", "address_line_2", "postcode", "name"], + set(), + ), + ( + """ + email address,colour + """, + "email", + ["email address", "colour"], + set(["name"]), + ), + ( + """ + address_line_1, address_line_2, name + """, + "letter", + ["address_line_1", "address_line_2", "name"], + set(), + ), + ( + """ + phone number,list,list,name,list + """, + "sms", + ["phone number", "list", "name"], + set(), + ), + ], +) +def test_column_headers(file_contents, template_type, expected, expected_missing): + recipients = RecipientCSV( + file_contents, template=_sample_template(template_type, "((name))") + ) + assert recipients.column_headers == expected + assert recipients.missing_column_headers == expected_missing + assert recipients.has_errors == bool(expected_missing) + + +@pytest.mark.parametrize( + "content", + [ + "hello", + "hello ((name))", + ], +) +@pytest.mark.parametrize( + "file_contents,template_type", + [ + pytest.param("", "sms", marks=pytest.mark.xfail), + pytest.param("name", "sms", marks=pytest.mark.xfail), + pytest.param("email address", "sms", marks=pytest.mark.xfail), + pytest.param( + "address_line_1", + "letter", + marks=pytest.mark.xfail, + ), + pytest.param( + "address_line_1, address_line_2", + "letter", + marks=pytest.mark.xfail, + ), + pytest.param( + "address_line_6, postcode", + "letter", + marks=pytest.mark.xfail, + ), + pytest.param( + "address_line_1, postcode, address_line_7", + "letter", + marks=pytest.mark.xfail, + ), + ("phone number", "sms"), + ("phone number,name", "sms"), + ("email address", "email"), + ("email address,name", "email"), + ("PHONENUMBER", "sms"), + ("email_address", "email"), + ("address_line_1, address_line_2, postcode", "letter"), + ("address_line_1, address_line_2, address_line_7", "letter"), + ("address_line_1, address_line_2, address_line_3", "letter"), + ("address_line_4, address_line_5, address_line_6", "letter"), + ( + "address_line_1, address_line_2, address_line_3, address_line_4, address_line_5, address_line_6, postcode", + "letter", + ), + ], +) +def test_recipient_column(content, file_contents, template_type): + assert RecipientCSV( + file_contents, template=_sample_template(template_type, content) + ).has_recipient_columns + + +@pytest.mark.parametrize( + "file_contents,template_type,rows_with_bad_recipients,rows_with_missing_data", + [ + ( + """ + phone number,name,date + 2348675309,test1,test1 + 2348675309,test1 + +44 123,test1,test1 + 2348675309,test1,test1 + 2348675309,test1 + +1644000000,test1,test1 + ,test1,test1 + """, + "sms", + {2, 5}, + {1, 4, 6}, + ), + ( + """ + phone number,name + 2348675309,test1,test2 + """, + "sms", + set(), + set(), + ), + ( + """ + """, + "sms", + set(), + set(), + ), + ( + # missing postcode + """ + address_line_1,address_line_2,address_line_3,address_line_4,address_line_5,postcode,date + name, building, street, town, county, SE1 7LS,today + name, building, street, town, county, , today + """, + "letter", + {1}, + set(), + ), + ( + # not enough address fields + """ + address_line_1, postcode, date + name, SE1 7LS, today + """, + "letter", + {0}, + set(), + ), + ( + # optional address fields not filled in + """ + address_line_1,address_line_2,address_line_3,address_line_4,address_line_5,postcode,date + name ,123 fake st. , , , ,SE1 7LS,today + name , , , , ,SE1 7LS,today + """, + "letter", + {1}, + set(), + ), + ( + # Can use any address columns + """ + address_line_3, address_line_4, address_line_7, date + name , 123 fake st., SE1 7LS, today + """, + "letter", + set(), + set(), + ), + ], +) +@pytest.mark.parametrize( + "partial_instance", + [ + partial(RecipientCSV), + partial(RecipientCSV, allow_international_sms=False), + ], +) +def test_bad_or_missing_data( + file_contents, + template_type, + rows_with_bad_recipients, + rows_with_missing_data, + partial_instance, +): + recipients = partial_instance( + file_contents, template=_sample_template(template_type, "((date))") + ) + assert _index_rows(recipients.rows_with_bad_recipients) == rows_with_bad_recipients + assert _index_rows(recipients.rows_with_missing_data) == rows_with_missing_data + if rows_with_bad_recipients or rows_with_missing_data: + assert recipients.has_errors is True + + +@pytest.mark.parametrize( + "file_contents,rows_with_bad_recipients", + [ + ( + """ + phone number + +800000000000 + 1234 + +447900123 + """, + {0, 1, 2}, + ), + ( + """ + phone number, country + 1-202-234-0104, USA + +12022340104, USA + +23051234567, Mauritius + """, + {2}, + ), + ], +) +def test_international_recipients(file_contents, rows_with_bad_recipients): + recipients = RecipientCSV( + file_contents, + template=_sample_template("sms"), + allow_international_sms=True, + ) + assert _index_rows(recipients.rows_with_bad_recipients) == rows_with_bad_recipients + + +def test_errors_when_too_many_rows(): + recipients = RecipientCSV( + "email address\n" + ("a@b.com\n" * 101), + template=_sample_template("email"), + ) + + # Confirm the normal max_row limit + assert recipients.max_rows == 100_000 + # Override to make this test faster + recipients.max_rows = 100 + + assert recipients.too_many_rows is True + assert recipients.has_errors is True + assert recipients.rows[99]["email_address"].data == "a@b.com" + # We stop processing subsequent rows + assert recipients.rows[100] is None + + +@pytest.mark.parametrize( + "file_contents,template_type,guestlist,count_of_rows_with_errors", + [ + ( + """ + phone number + 2348675309 + 2348675301 + 2348675302 + 2348675303 + """, + "sms", + ["+12348675309"], # Same as first phone number but in different format + 3, + ), + ( + """ + phone number + 12348675309 + 2348675301 + 2348675302 + """, + "sms", + [ + "2348675309", + "12348675301", + "2348675302", + "2341231234", + "test@example.com", + ], + 0, + ), + ( + """ + email address + IN_GUESTLIST@EXAMPLE.COM + not_in_guestlist@example.com + """, + "email", + [ + "in_guestlist@example.com", + "2348675309", + ], # Email case differs to the one in the CSV + 1, + ), + ], +) +def test_recipient_guestlist( + file_contents, template_type, guestlist, count_of_rows_with_errors +): + recipients = RecipientCSV( + file_contents, template=_sample_template(template_type), guestlist=guestlist + ) + + if count_of_rows_with_errors: + assert not recipients.allowed_to_send_to + else: + assert recipients.allowed_to_send_to + + # Make sure the guestlist isn’t emptied by reading it. If it’s an iterator then + # there’s a risk that it gets emptied after being read once + recipients.guestlist = ( + str(fake_number) for fake_number in range(7700900888, 7700900898) + ) + list(recipients.guestlist) + assert not recipients.allowed_to_send_to + assert recipients.has_errors + + # An empty guestlist is treated as no guestlist at all + recipients.guestlist = [] + assert recipients.allowed_to_send_to + recipients.guestlist = itertools.chain() + assert recipients.allowed_to_send_to + + +def test_detects_rows_which_result_in_overly_long_messages(): + template = SMSMessageTemplate( + {"content": "((placeholder))", "template_type": "sms"}, + sender=None, + prefix=None, + ) + recipients = RecipientCSV( + """ + phone number,placeholder + 2348675309,1 + 2348675301,{one_under} + 2348675302,{exactly} + 2348675303,{one_over} + """.format( + one_under="a" * (SMS_CHAR_COUNT_LIMIT - 1), + exactly="a" * SMS_CHAR_COUNT_LIMIT, + one_over="a" * (SMS_CHAR_COUNT_LIMIT + 1), + ), + template=template, + ) + assert _index_rows(recipients.rows_with_errors) == {3} + assert _index_rows(recipients.rows_with_message_too_long) == {3} + assert recipients.has_errors + assert recipients[0].has_error_spanning_multiple_cells is False + assert recipients[1].has_error_spanning_multiple_cells is False + assert recipients[2].has_error_spanning_multiple_cells is False + assert recipients[3].has_error_spanning_multiple_cells is True + + +def test_detects_rows_which_result_in_empty_messages(): + template = SMSMessageTemplate( + {"content": "((show??content))", "template_type": "sms"}, + sender=None, + prefix=None, + ) + recipients = RecipientCSV( + """ + phone number,show + 2348675309,yes + 2348675301,no + 2348675302,yes + """, + template=template, + ) + assert _index_rows(recipients.rows_with_errors) == {1} + assert _index_rows(recipients.rows_with_empty_message) == {1} + assert recipients.has_errors + assert recipients[0].has_error_spanning_multiple_cells is False + assert recipients[1].has_error_spanning_multiple_cells is True + assert recipients[2].has_error_spanning_multiple_cells is False + + +@pytest.mark.parametrize( + "key, expected", + sum( + [ + [(key, expected) for key in group] + for expected, group in [ + ( + "2348675309", + ( + "phone number", + " PHONENUMBER", + "phone_number", + "phone-number", + "phoneNumber", + ), + ), + ( + "Jo", + ( + "FIRSTNAME", + "first name", + "first_name ", + "first-name", + "firstName", + ), + ), + ( + "Bloggs", + ( + "Last Name", + "LASTNAME", + " last_name", + "last-name", + "lastName ", + ), + ), + ] + ], + [], + ), +) +def test_ignores_spaces_and_case_in_placeholders(key, expected): + recipients = RecipientCSV( + """ + phone number,FIRSTNAME, Last Name + 2348675309, Jo, Bloggs + """, + template=_sample_template( + "sms", content="((phone_number)) ((First Name)) ((lastname))" + ), + ) + first_row = recipients[0] + assert first_row.get(key).data == expected + assert first_row[key].data == expected + assert first_row.recipient == "2348675309" + assert len(first_row.items()) == 3 + assert not recipients.has_errors + + assert recipients.missing_column_headers == set() + recipients.placeholders = {"one", "TWO", "Thirty_Three"} + assert recipients.missing_column_headers == {"one", "TWO", "Thirty_Three"} + assert recipients.has_errors + + +@pytest.mark.parametrize( + "character, name", + ( + (" ", "SPACE"), + # these ones don’t have unicode names + ("\n", None), # newline + ("\r", None), # carriage return + ("\t", None), # tab + ("\u180E", "MONGOLIAN VOWEL SEPARATOR"), + ("\u200B", "ZERO WIDTH SPACE"), + ("\u200C", "ZERO WIDTH NON-JOINER"), + ("\u200D", "ZERO WIDTH JOINER"), + ("\u2060", "WORD JOINER"), + ("\uFEFF", "ZERO WIDTH NO-BREAK SPACE"), + # all the things + (" \n\r\t\u000A\u000D\u180E\u200B\u200C\u200D\u2060\uFEFF", None), + ), +) +def test_ignores_leading_whitespace_in_file(character, name): + if name is not None: + assert unicodedata.name(character) == name + + recipients = RecipientCSV( + "{}emailaddress\ntest@example.com".format(character), + template=_sample_template("email"), + ) + first_row = recipients[0] + + assert recipients.column_headers == ["emailaddress"] + assert recipients.recipient_column_headers == ["email address"] + assert recipients.missing_column_headers == set() + assert recipients.placeholders == ["email address"] + + assert first_row.get("email address").data == "test@example.com" + assert first_row["email address"].data == "test@example.com" + assert first_row.recipient == "test@example.com" + + assert not recipients.has_errors + + +def test_error_if_too_many_recipients(): + recipients = RecipientCSV( + "phone number,\n2348675309,\n2348675309,\n2348675309,", + template=_sample_template("sms"), + remaining_messages=2, + ) + assert recipients.has_errors + assert recipients.more_rows_than_can_send + + +def test_dont_error_if_too_many_recipients_not_specified(): + recipients = RecipientCSV( + "phone number,\n2348675309,\n2348675309,\n2348675309,", + template=_sample_template("sms"), + ) + assert not recipients.has_errors + assert not recipients.more_rows_than_can_send + + +@pytest.mark.parametrize( + "index, expected_row", + [ + ( + 0, + { + "phone number": "07700 90000 1", + "colour": "red", + }, + ), + ( + 1, + { + "phone_number": "07700 90000 2", + "COLOUR": "green", + }, + ), + ( + 2, + {"p h o n e n u m b e r": "07700 90000 3", " colour ": "blue"}, + ), + pytest.param( + 3, + {"phone number": "foo"}, + marks=pytest.mark.xfail(raises=IndexError), + ), + ( + -1, + {"p h o n e n u m b e r": "07700 90000 3", " colour ": "blue"}, + ), + ], +) +def test_recipients_can_be_accessed_by_index(index, expected_row): + recipients = RecipientCSV( + """ + phone number, colour + 07700 90000 1, red + 07700 90000 2, green + 07700 90000 3, blue + """, + template=_sample_template("sms"), + ) + for key, value in expected_row.items(): + assert recipients[index][key].data == value + + +@pytest.mark.parametrize("international_sms", (True, False)) +def test_multiple_sms_recipient_columns(international_sms): + recipients = RecipientCSV( + """ + phone number, phone number, phone_number, foo + 234-867-5301, 234-867-5302, 234-867-5309, bar + """, + template=_sample_template("sms"), + allow_international_sms=international_sms, + ) + assert recipients.column_headers == ["phone number", "phone_number", "foo"] + assert ( + recipients.column_headers_as_column_keys == dict(phonenumber="", foo="").keys() + ) + assert recipients.rows[0].get("phone number").data == ("234-867-5309") + assert recipients.rows[0].get("phone_number").data == ("234-867-5309") + assert recipients.rows[0].get("phone number").error is None + assert recipients.duplicate_recipient_column_headers == OrderedSet( + ["phone number", "phone_number"] + ) + assert recipients.has_errors + + +@pytest.mark.parametrize( + "column_name", + ( + "phone_number", + "phonenumber", + "phone number", + "phone-number", + "p h o n e n u m b e r", + ), +) +def test_multiple_sms_recipient_columns_with_missing_data(column_name): + recipients = RecipientCSV( + """ + names, phone number, {} + "Joanna and Steve", 07900 900111 + """.format( + column_name + ), + template=_sample_template("sms"), + allow_international_sms=True, + ) + expected_column_headers = ["names", "phone number"] + if column_name != "phone number": + expected_column_headers.append(column_name) + assert recipients.column_headers == expected_column_headers + assert ( + recipients.column_headers_as_column_keys + == dict(phonenumber="", names="").keys() + ) + # A piece of weirdness uncovered: since rows are created before spaces in column names are normalised, when + # there are duplicate recipient columns and there is data for only one of the columns, if the columns have the same + # spacing, phone number data will be a list of this one phone number and None, while if the spacing style differs + # between two duplicate column names, the phone number data will be None. If there are no duplicate columns + # then our code finds the phone number well regardless of the spacing, so this should not affect our users. + phone_number_data = None + if column_name == "phone number": + phone_number_data = ["07900 900111", None] + assert recipients.rows[0]["phonenumber"].data == phone_number_data + assert recipients.rows[0].get("phone number").error is None + expected_duplicated_columns = ["phone number"] + if column_name != "phone number": + expected_duplicated_columns.append(column_name) + assert recipients.duplicate_recipient_column_headers == OrderedSet( + expected_duplicated_columns + ) + assert recipients.has_errors + + +def test_multiple_email_recipient_columns(): + recipients = RecipientCSV( + """ + EMAILADDRESS, email_address, foo + one@two.com, two@three.com, bar + """, + template=_sample_template("email"), + ) + assert recipients.rows[0].get("email address").data == ("two@three.com") + assert recipients.rows[0].get("email address").error is None + assert recipients.has_errors + assert recipients.duplicate_recipient_column_headers == OrderedSet( + ["EMAILADDRESS", "email_address"] + ) + assert recipients.has_errors + + +def test_multiple_letter_recipient_columns(): + recipients = RecipientCSV( + """ + address line 1, Address Line 2, address line 1, address_line_2 + 1,2,3,4 + """, + template=_sample_template("letter"), + ) + assert recipients.rows[0].get("addressline1").data == ("3") + assert recipients.rows[0].get("addressline1").error is None + assert recipients.has_errors + assert recipients.duplicate_recipient_column_headers == OrderedSet( + ["address line 1", "Address Line 2", "address line 1", "address_line_2"] + ) + assert recipients.has_errors + + +def test_displayed_rows_when_some_rows_have_errors(): + recipients = RecipientCSV( + """ + email address, name + a@b.com, + a@b.com, + a@b.com, My Name + a@b.com, + a@b.com, + """, + template=_sample_template("email", "((name))"), + max_errors_shown=3, + ) + + assert len(list(recipients.displayed_rows)) == 3 + + +def test_displayed_rows_when_there_are_no_rows_with_errors(): + recipients = RecipientCSV( + """ + email address, name + a@b.com, My Name + a@b.com, My Name + a@b.com, My Name + a@b.com, My Name + """, + template=_sample_template("email", "((name))"), + max_errors_shown=3, + ) + + assert len(list(recipients.displayed_rows)) == 4 + + +def test_multi_line_placeholders_work(): + recipients = RecipientCSV( + """ + email address, data + a@b.com, "a\nb\n\nc" + """, + template=_sample_template("email", "((data))"), + ) + + assert recipients.rows[0].personalisation["data"] == "a\nb\n\nc" + + +@pytest.mark.parametrize( + "extra_args, expected_errors, expected_bad_rows", + ( + ({}, True, {0}), + ({"allow_international_letters": False}, True, {0}), + ({"allow_international_letters": True}, False, set()), + ), +) +def test_accepts_international_addresses_when_allowed( + extra_args, expected_errors, expected_bad_rows +): + recipients = RecipientCSV( + """ + address line 1, address line 2, address line 3 + First Lastname, 123 Example St, Fiji + First Lastname, 123 Example St, SW1A 1AA + """, + template=_sample_template("letter"), + **extra_args, + ) + assert recipients.has_errors is expected_errors + assert _index_rows(recipients.rows_with_bad_recipients) == expected_bad_rows + # Prove that the error isn’t because the given country is unknown + assert recipients[0].as_postal_address.country == Country("Fiji") + + +def test_address_validation_speed(): + # We should be able to validate 1000 lines of address data in about + # a second – if it starts to get slow, something is inefficient + number_of_lines = 1000 + uk_addresses_with_valid_postcodes = "\n".join( + ( + "{n} Example Street, London, {a}{b} {c}{d}{e}".format( + n=randrange(1000), + a=choice(["n", "e", "sw", "se", "w"]), + b=choice(range(1, 10)), + c=choice(range(1, 10)), + d=choice("ABDefgHJLNPqrstUWxyZ"), + e=choice("ABDefgHJLNPqrstUWxyZ"), + ) + for i in range(number_of_lines) + ) + ) + recipients = RecipientCSV( + "address line 1, address line 2, address line 3\n" + + (uk_addresses_with_valid_postcodes), + template=_sample_template("letter"), + allow_international_letters=False, + ) + for row in recipients: + assert not row.has_bad_postal_address + + +def test_email_validation_speed(): + email_addresses = "\n".join( + ( + "{a}{b}@example-{n}.com,Example,Thursday".format( + n=randrange(1000), + a=choice(string.ascii_letters), + b=choice(string.ascii_letters), + ) + for i in range(1000) + ) + ) + recipients = RecipientCSV( + "email address,name,day\n" + email_addresses, + template=_sample_template( + "email", + content=f""" + hello ((name)) today is ((day)) + here’s the letter ‘a’ 1000 times: + {'a' * 1000} + """, + ), + ) + for row in recipients: + assert not row.has_error + + +@pytest.mark.parametrize("should_validate", [True, False]) +def test_recipient_csv_checks_should_validate_flag(should_validate): + template = _sample_template("sms") + template.is_message_empty = Mock(return_value=False) + + recipients = RecipientCSV( + """phone number,name + 2348675309, test1 + +447700 900 460,test2""", + template=template, + should_validate=should_validate, + ) + + recipients._get_error_for_field = Mock(return_value=None) + + list(recipients.get_rows()) + + assert template.is_message_empty.called is should_validate + assert recipients._get_error_for_field.called is should_validate diff --git a/tests/notification_utils/test_recipient_validation.py b/tests/notification_utils/test_recipient_validation.py new file mode 100644 index 000000000..91360056e --- /dev/null +++ b/tests/notification_utils/test_recipient_validation.py @@ -0,0 +1,428 @@ +import pytest + +from notifications_utils.recipients import ( + InvalidEmailError, + InvalidPhoneError, + allowed_to_send_to, + format_phone_number_human_readable, + format_recipient, + get_international_phone_info, + international_phone_info, + is_us_phone_number, + try_validate_and_format_phone_number, + validate_and_format_phone_number, + validate_email_address, + validate_phone_number, +) + +valid_us_phone_numbers = [ + "1-202-555-0104", + "+12025550104", + "12025550104", + "2025550104", + "(202) 555-0104", +] + +# TODO +# International phone number tests are commented out as a result of issue #943 in notifications-admin. We are +# deliberately eliminating the ability to send to numbers outside of country code 1. These tests should +# be removed at some point when we are sure we are never going to support international numbers + +valid_international_phone_numbers = [ + # "+71234567890", # Russia + # "+447123456789", # UK + # "+4407123456789", # UK + # "+4407123 456789", # UK + # "+4407123-456-789", # UK + # "+23051234567", # Mauritius, + # "+682 12345", # Cook islands + # "+3312345678", + # "+9-2345-12345-12345", # 15 digits +] + + +valid_phone_numbers = valid_us_phone_numbers + valid_international_phone_numbers + + +invalid_us_phone_numbers = sum( + [ + [(phone_number, error) for phone_number in group] + for error, group in [ + ( + "Too many digits", + ( + "55512345678", + "+155512345678", + "(555) 1234-5678", + ), + ), + ( + "Not enough digits", + ( + "555123123", + "(555) 123-123", + "7890x32109", + "07123 ☟☜âŦ‡âŦ†â˜žâ˜", + "07123☟☜âŦ‡âŦ†â˜žâ˜", + ), + ), + ("Phone number range is not in use", ("1555123123",)), + ("Phone number is not possible", ("07123 456789...",)), + ( + "The string supplied did not seem to be a phone number.", + ( + '07";DROP TABLE;"', + "ALPHANUM3R1C", + ), + ), + ] + ], + [], +) + + +invalid_phone_numbers = [ + ("+80233456789", "Not a valid country prefix"), + ("1234567", "Not enough digits"), + ("+682 1234", "Invalid country code"), # Cook Islands phone numbers can be 5 digits + ("+12345 12345 12345 6", "Too many digits"), +] + + +valid_email_addresses = ( + "email@domain.com", + "email@domain.COM", + "firstname.lastname@domain.com", + "firstname.o'lastname@domain.com", + "email@subdomain.domain.com", + "firstname+lastname@domain.com", + "1234567890@domain.com", + "email@domain-one.com", + "_______@domain.com", + "email@domain.name", + "email@domain.superlongtld", + "email@domain.co.jp", + "firstname-lastname@domain.com", + "info@german-financial-services.vermÃļgensberatung", + "info@german-financial-services.reallylongarbitrarytldthatiswaytoohugejustincase", + "japanese-info@䞋え.テ゚ト", + "email@double--hyphen.com", +) +invalid_email_addresses = ( + "email@123.123.123.123", + "email@[123.123.123.123]", + "plainaddress", + "@no-local-part.com", + "Outlook Contact ", + "no-at.domain.com", + "no-tld@domain", + ";beginning-semicolon@domain.co.uk", + "middle-semicolon@domain.co;uk", + "trailing-semicolon@domain.com;", + '"email+leading-quotes@domain.com', + 'email+middle"-quotes@domain.com', + '"quoted-local-part"@domain.com', + '"quoted@domain.com"', + "lots-of-dots@domain..gov..uk", + "two-dots..in-local@domain.com", + "multiple@domains@domain.com", + "spaces in local@domain.com", + "spaces-in-domain@dom ain.com", + "underscores-in-domain@dom_ain.com", + "pipe-in-domain@example.com|gov.uk", + "comma,in-local@gov.uk", + "comma-in-domain@domain,gov.uk", + "pound-sign-in-localÂŖ@domain.com", + "local-with-’-apostrophe@domain.com", + "local-with-”-quotes@domain.com", + "domain-starts-with-a-dot@.domain.com", + "brackets(in)local@domain.com", + "email-too-long-{}@example.com".format("a" * 320), + "incorrect-punycode@xn---something.com", +) + + +@pytest.mark.parametrize("phone_number", valid_international_phone_numbers) +def test_detect_international_phone_numbers(phone_number): + assert is_us_phone_number(phone_number) is False + + +@pytest.mark.parametrize("phone_number", valid_us_phone_numbers) +def test_detect_us_phone_numbers(phone_number): + assert is_us_phone_number(phone_number) is True + + +@pytest.mark.parametrize( + "phone_number, expected_info", + [ + # ( + # "+4407900900123", + # international_phone_info( + # international=True, + # country_prefix="44", # UK + # billable_units=1, + # ), + # ), + # ( + # "+4407700900123", + # international_phone_info( + # international=True, + # country_prefix="44", # Number in TV range + # billable_units=1, + # ), + # ), + # ( + # "+4407700800123", + # international_phone_info( + # international=True, + # country_prefix="44", # UK Crown dependency, so prefix same as UK + # billable_units=1, + # ), + # ), + # ( # + # "+20-12-1234-1234", + # international_phone_info( + # international=True, + # country_prefix="20", # Egypt + # billable_units=1, + # ), + # ), + # ( + # "+201212341234", + # international_phone_info( + # international=True, + # country_prefix="20", # Egypt + # billable_units=1, + # ), + # ), + ( + "+1 664-491-3434", + international_phone_info( + international=True, + country_prefix="1664", # Montserrat + billable_units=1, + ), + ), + # ( + # "+71234567890", + # international_phone_info( + # international=True, + # country_prefix="7", # Russia + # billable_units=1, + # ), + # ), + ( + "1-202-555-0104", + international_phone_info( + international=False, + country_prefix="1", # USA + billable_units=1, + ), + ), + ( + "202-555-0104", + international_phone_info( + international=False, + country_prefix="1", # USA + billable_units=1, + ), + ), + # ( + # "+23051234567", + # international_phone_info( + # international=True, + # country_prefix="230", # Mauritius + # billable_units=1, + # ), + # ), + ], +) +def test_get_international_info(phone_number, expected_info): + assert get_international_phone_info(phone_number) == expected_info + + +@pytest.mark.parametrize( + "phone_number", + [ + "+21 4321 0987", + "+00997 1234 7890", + "+801234-7890", + "+8-0-1234-78901", + ], +) +def test_get_international_info_raises(phone_number): + with pytest.raises(InvalidPhoneError) as error: + get_international_phone_info(phone_number) + assert str(error.value) == "Not a valid country prefix" + + +@pytest.mark.parametrize("phone_number", valid_us_phone_numbers) +@pytest.mark.parametrize( + "extra_args", + [ + {}, + {"international": False}, + ], +) +def test_phone_number_accepts_valid_values(extra_args, phone_number): + try: + validate_phone_number(phone_number, **extra_args) + except InvalidPhoneError: + pytest.fail("Unexpected InvalidPhoneError") + + +@pytest.mark.parametrize("phone_number", valid_phone_numbers) +def test_phone_number_accepts_valid_international_values(phone_number): + try: + validate_phone_number(phone_number, international=True) + except InvalidPhoneError: + pytest.fail("Unexpected InvalidPhoneError") + + +@pytest.mark.parametrize("phone_number", valid_us_phone_numbers) +def test_valid_us_phone_number_can_be_formatted_consistently(phone_number): + assert validate_and_format_phone_number(phone_number) == "+12025550104" + + +@pytest.mark.parametrize( + "phone_number, expected_formatted", + [ + # ("+44071234567890", "+4471234567890"), + ("1-202-555-0104", "+12025550104"), + ("+12025550104", "+12025550104"), + ("12025550104", "+12025550104"), + ("+12025550104", "+12025550104"), + # ("+23051234567", "+23051234567"), + ], +) +def test_valid_international_phone_number_can_be_formatted_consistently( + phone_number, expected_formatted +): + assert ( + validate_and_format_phone_number(phone_number, international=True) + == expected_formatted + ) + + +@pytest.mark.parametrize("phone_number, error_message", invalid_us_phone_numbers) +@pytest.mark.parametrize( + "extra_args", + [ + {}, + {"international": False}, + ], +) +def test_phone_number_rejects_invalid_values(extra_args, phone_number, error_message): + with pytest.raises(InvalidPhoneError) as e: + validate_phone_number(phone_number, **extra_args) + assert error_message == str(e.value) + + +@pytest.mark.parametrize("phone_number, error_message", invalid_phone_numbers) +def test_phone_number_rejects_invalid_international_values(phone_number, error_message): + with pytest.raises(InvalidPhoneError) as e: + validate_phone_number(phone_number, international=True) + assert error_message == str(e.value) + + +@pytest.mark.parametrize("email_address", valid_email_addresses) +def test_validate_email_address_accepts_valid(email_address): + try: + assert validate_email_address(email_address) == email_address + except InvalidEmailError: + pytest.fail("Unexpected InvalidEmailError") + + +@pytest.mark.parametrize( + "email", + [ + " email@domain.com ", + "\temail@domain.com", + "\temail@domain.com\n", + "\u200Bemail@domain.com\u200B", + ], +) +def test_validate_email_address_strips_whitespace(email): + assert validate_email_address(email) == "email@domain.com" + + +@pytest.mark.parametrize("email_address", invalid_email_addresses) +def test_validate_email_address_raises_for_invalid(email_address): + with pytest.raises(InvalidEmailError) as e: + validate_email_address(email_address) + assert str(e.value) == "Not a valid email address" + + +@pytest.mark.parametrize("phone_number", valid_us_phone_numbers) +def test_validates_against_guestlist_of_phone_numbers(phone_number): + assert allowed_to_send_to( + phone_number, ["2025550104", "2025550105", "test@example.com"] + ) + assert not allowed_to_send_to( + phone_number, ["2025550105", "2028675309", "test@example.com"] + ) + + +# @pytest.mark.parametrize( +# "recipient_number, allowlist_number", +# [ +# ["+4407123-456-789", "+4407123456789"], +# ["+4407123456789", "+4407123-456-789"], +# ], +# ) +# def test_validates_against_guestlist_of_international_phone_numbers( +# recipient_number, allowlist_number +# ): +# assert allowed_to_send_to(recipient_number, [allowlist_number]) + + +@pytest.mark.parametrize("email_address", valid_email_addresses) +def test_validates_against_guestlist_of_email_addresses(email_address): + assert not allowed_to_send_to( + email_address, ["very_special_and_unique@example.com"] + ) + + +@pytest.mark.parametrize( + "phone_number, expected_formatted", + [ + # ("+4407900900123", "+44 7900 900123"), # UK + # ("+44(0)7900900123", "+44 7900 900123"), # UK + # ("+447900900123", "+44 7900 900123"), # UK + # ("+20-12-1234-1234", "+20 121 234 1234"), # Egypt + # ("+201212341234", "+20 121 234 1234"), # Egypt + ("+1 664 491-3434", "+1 664-491-3434"), # Montserrat + # ("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia) + ("1-202-555-0104", "(202) 555-0104"), # Washington DC (USA) + # ("+23051234567", "+230 5123 4567"), # Mauritius + # ("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France) + ], +) +def test_format_us_and_international_phone_numbers(phone_number, expected_formatted): + assert format_phone_number_human_readable(phone_number) == expected_formatted + + +@pytest.mark.parametrize( + "recipient, expected_formatted", + [ + (True, ""), + (False, ""), + (0, ""), + (0.1, ""), + (None, ""), + ("foo", "foo"), + ("TeSt@ExAmPl3.com", "test@exampl3.com"), + # ("+4407900 900 123", "+447900900123"), + ("+1 800 555 5555", "+18005555555"), + ], +) +def test_format_recipient(recipient, expected_formatted): + assert format_recipient(recipient) == expected_formatted + + +def test_try_format_recipient_doesnt_throw(): + assert try_validate_and_format_phone_number("ALPHANUM3R1C") == "ALPHANUM3R1C" + + +def test_format_phone_number_human_readable_doenst_throw(): + assert format_phone_number_human_readable("ALPHANUM3R1C") == "ALPHANUM3R1C" diff --git a/tests/notification_utils/test_request_header_authentication.py b/tests/notification_utils/test_request_header_authentication.py new file mode 100644 index 000000000..f595619ce --- /dev/null +++ b/tests/notification_utils/test_request_header_authentication.py @@ -0,0 +1,61 @@ +import pytest +from werkzeug.test import EnvironBuilder + +from notifications_utils.request_helper import NotifyRequest, _check_proxy_header_secret + + +@pytest.mark.parametrize( + "header,secrets,expected", + [ + ( + {"X-Custom-Forwarder": "right_key"}, + ["right_key", "old_key"], + (True, "Key used: 1"), + ), + ({"X-Custom-Forwarder": "right_key"}, ["right_key"], (True, "Key used: 1")), + ({"X-Custom-Forwarder": "right_key"}, ["right_key", ""], (True, "Key used: 1")), + ({"My-New-Header": "right_key"}, ["right_key", ""], (True, "Key used: 1")), + ({"X-Custom-Forwarder": "right_key"}, ["", "right_key"], (True, "Key used: 2")), + ( + {"X-Custom-Forwarder": "right_key"}, + ["", "old_key", "right_key"], + (True, "Key used: 3"), + ), + ( + {"X-Custom-Forwarder": ""}, + ["right_key", "old_key"], + (False, "Header exists but is empty"), + ), + ( + {"X-Custom-Forwarder": "right_key"}, + ["", None], + (False, "Secrets are not configured"), + ), + ( + {"X-Custom-Forwarder": "wrong_key"}, + ["right_key", "old_key"], + (False, "Header didn't match any keys"), + ), + ], +) +def test_request_header_authorization(header, secrets, expected): + builder = EnvironBuilder() + builder.headers.extend(header) + request = NotifyRequest(builder.get_environ()) + + res = _check_proxy_header_secret(request, secrets, list(header.keys())[0]) + assert res == expected + + +@pytest.mark.parametrize( + "secrets,expected", + [ + (["old_key", "right_key"], (False, "Header missing")), + ], +) +def test_request_header_authorization_missing_header(secrets, expected): + builder = EnvironBuilder() + request = NotifyRequest(builder.get_environ()) + + res = _check_proxy_header_secret(request, secrets) + assert res == expected diff --git a/tests/notification_utils/test_request_id.py b/tests/notification_utils/test_request_id.py new file mode 100644 index 000000000..fee5e7d87 --- /dev/null +++ b/tests/notification_utils/test_request_id.py @@ -0,0 +1,32 @@ +from notifications_utils import request_helper + + +def test_request_id_is_set_on_response(app): + request_helper.init_app(app) + client = app.test_client() + + with app.app_context(): + response = client.get( + "/", headers={"X-B3-TraceId": "generated", "X-B3-SpanId": "generated"} + ) + assert response.headers["X-B3-TraceId"] == "generated" + assert response.headers["X-B3-SpanId"] == "generated" + + +def test_request_id_is_set_on_error_response(app): + request_helper.init_app(app) + client = app.test_client() + # turn off DEBUG so that the flask default error handler gets triggered + app.config["DEBUG"] = False + + @app.route("/") + def error_route(): + raise Exception() + + with app.app_context(): + response = client.get( + "/", headers={"X-B3-TraceId": "generated", "X-B3-SpanId": "generated"} + ) + assert response.status_code == 500 + assert response.headers["X-B3-TraceId"] == "generated" + assert response.headers["X-B3-SpanId"] == "generated" diff --git a/tests/notification_utils/test_s3.py b/tests/notification_utils/test_s3.py new file mode 100644 index 000000000..46b863c4f --- /dev/null +++ b/tests/notification_utils/test_s3.py @@ -0,0 +1,108 @@ +from urllib.parse import parse_qs + +import botocore +import pytest + +from notifications_utils.s3 import S3ObjectNotFound, s3download, s3upload + +contents = "some file data" +region = "eu-west-1" +bucket = "some_bucket" +location = "some_file_location" +content_type = "binary/octet-stream" + + +def test_s3upload_save_file_to_bucket(mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + s3upload( + filedata=contents, region=region, bucket_name=bucket, file_location=location + ) + mocked_put = mocked.return_value.Object.return_value.put + mocked_put.assert_called_once_with( + Body=contents, + ServerSideEncryption="AES256", + ContentType=content_type, + ) + + +def test_s3upload_save_file_to_bucket_with_contenttype(mocker): + content_type = "image/png" + mocked = mocker.patch("notifications_utils.s3.Session.resource") + s3upload( + filedata=contents, + region=region, + bucket_name=bucket, + file_location=location, + content_type=content_type, + ) + mocked_put = mocked.return_value.Object.return_value.put + mocked_put.assert_called_once_with( + Body=contents, + ServerSideEncryption="AES256", + ContentType=content_type, + ) + + +def test_s3upload_raises_exception(app, mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + response = {"Error": {"Code": 500}} + exception = botocore.exceptions.ClientError(response, "Bad exception") + mocked.return_value.Object.return_value.put.side_effect = exception + with pytest.raises(botocore.exceptions.ClientError): + s3upload( + filedata=contents, + region=region, + bucket_name=bucket, + file_location="location", + ) + + +def test_s3upload_save_file_to_bucket_with_urlencoded_tags(mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + s3upload( + filedata=contents, + region=region, + bucket_name=bucket, + file_location=location, + tags={"a": "1/2", "b": "x y"}, + ) + mocked_put = mocked.return_value.Object.return_value.put + + # make sure tags were a urlencoded query string + encoded_tags = mocked_put.call_args[1]["Tagging"] + assert parse_qs(encoded_tags) == {"a": ["1/2"], "b": ["x y"]} + + +def test_s3upload_save_file_to_bucket_with_metadata(mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + s3upload( + filedata=contents, + region=region, + bucket_name=bucket, + file_location=location, + metadata={"status": "valid", "pages": "5"}, + ) + mocked_put = mocked.return_value.Object.return_value.put + + metadata = mocked_put.call_args[1]["Metadata"] + assert metadata == {"status": "valid", "pages": "5"} + + +def test_s3download_gets_file(mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + mocked_object = mocked.return_value.Object + mocked_get = mocked.return_value.Object.return_value.get + s3download("bucket", "location.file") + mocked_object.assert_called_once_with("bucket", "location.file") + mocked_get.assert_called_once_with() + + +def test_s3download_raises_on_error(mocker): + mocked = mocker.patch("notifications_utils.s3.Session.resource") + mocked.return_value.Object.side_effect = botocore.exceptions.ClientError( + {"Error": {"Code": 404}}, + "Bad exception", + ) + + with pytest.raises(S3ObjectNotFound): + s3download("bucket", "location.file") diff --git a/tests/notification_utils/test_safe_string.py b/tests/notification_utils/test_safe_string.py new file mode 100644 index 000000000..a0bf3360f --- /dev/null +++ b/tests/notification_utils/test_safe_string.py @@ -0,0 +1,47 @@ +import pytest + +from notifications_utils.safe_string import ( + make_string_safe_for_email_local_part, + make_string_safe_for_id, +) + + +@pytest.mark.parametrize( + "unsafe_string, expected_safe", + [ + ("name with spaces", "name.with.spaces"), + ("singleword", "singleword"), + ("UPPER CASE", "upper.case"), + ("Service - with dash", "service.with.dash"), + ("lots of spaces", "lots.of.spaces"), + ("name.with.dots", "name.with.dots"), + ("name-with-other-delimiters", "namewithotherdelimiters"), + (".leading", "leading"), + ("trailing.", "trailing"), + ("ÃŧńïçÃļdÃĢ wÃļrdś", "unicode.words"), + ], +) +def test_email_safe_return_dot_separated_email_local_part(unsafe_string, expected_safe): + assert make_string_safe_for_email_local_part(unsafe_string) == expected_safe + + +@pytest.mark.parametrize( + "unsafe_string, expected_safe", + [ + ("name with spaces", "name-with-spaces"), + ("singleword", "singleword"), + ("UPPER CASE", "upper-case"), + ("Service - with dash", "service---with-dash"), + ("lots of spaces", "lots-of-spaces"), + ("name.with.dots", "namewithdots"), + ("name-with-dashes", "name-with-dashes"), + ("N. London", "n-london"), + (".leading", "leading"), + ("-leading", "-leading"), + ("trailing.", "trailing"), + ("trailing-", "trailing-"), + ("ÃŧńïçÃļdÃĢ wÃļrdś", "unicode-words"), + ], +) +def test_id_safe_return_dash_separated_string(unsafe_string, expected_safe): + assert make_string_safe_for_id(unsafe_string) == expected_safe diff --git a/tests/notification_utils/test_sanitise_text.py b/tests/notification_utils/test_sanitise_text.py new file mode 100644 index 000000000..062f280e0 --- /dev/null +++ b/tests/notification_utils/test_sanitise_text.py @@ -0,0 +1,313 @@ +import pytest + +from notifications_utils.sanitise_text import SanitiseASCII, SanitiseSMS, SanitiseText + +params, ids = zip( + (("a", "a"), "ascii char (a)"), + # ascii control char (not in GSM) + (("\t", " "), "ascii control char not in gsm (tab)"), + # TODO we support lots of languages now not in the GSM charset so maybe make this 'downgrading' go away + # TODO for now comment out this line because it directly conflicts with support for Turkish + # these are not in GSM charset so are downgraded + # (("ç", "c"), "decomposed unicode char (C with cedilla)"), + # these unicode chars should change to something completely different for compatibility + # (("–", "-"), "compatibility transform unicode char (EN DASH (U+2013)"), + # (("—", "-"), "compatibility transform unicode char (EM DASH (U+2014)"), + ( + ("â€Ļ", "..."), + "compatibility transform unicode char (HORIZONTAL ELLIPSIS (U+2026)", + ), + (("\u200B", ""), "compatibility transform unicode char (ZERO WIDTH SPACE (U+200B)"), + ( + ("‘", "'"), + "compatibility transform unicode char (LEFT SINGLE QUOTATION MARK (U+2018)", + ), + ( + ("’", "'"), + "compatibility transform unicode char (RIGHT SINGLE QUOTATION MARK (U+2019)", + ), + # Conflict with Chinese quotes + # ( + # ("“", '"'), + # "compatibility transform unicode char (LEFT DOUBLE QUOTATION MARK (U+201C) ", + # ), + # ( + # ("”", '"'), + # "compatibility transform unicode char (RIGHT DOUBLE QUOTATION MARK (U+201D)", + # ), + (("\xa0", " "), "nobreak transform unicode char (NO-BREAK SPACE (U+00A0))"), + # this unicode char is not decomposable + (("đŸ˜Ŧ", "?"), "undecomposable unicode char (grimace emoji)"), + (("↉", "?"), "vulgar fraction (↉) that we do not try decomposing"), +) + + +@pytest.mark.parametrize("char, expected", params, ids=ids) +@pytest.mark.parametrize("cls", [SanitiseSMS, SanitiseASCII]) +def test_encode_chars_the_same_for_ascii_and_sms(char, expected, cls): + assert cls.encode_char(char) == expected + + +params, ids = zip( + # ascii control chars are allowed in GSM but not in ASCII + (("\n", "\n", "?"), "ascii control char in gsm (newline)"), + (("\r", "\r", "?"), "ascii control char in gsm (return)"), + # These characters are present in GSM but not in ascii + (("à", "à", "a"), "non-ascii gsm char (a with accent)"), + (("â‚Ŧ", "â‚Ŧ", "?"), "non-ascii gsm char (euro)"), + # These characters are Welsh characters that are not present in GSM + (("Ãĸ", "Ãĸ", "a"), "non-gsm Welsh char (a with hat)"), + (("Åļ", "Åļ", "Y"), "non-gsm Welsh char (capital y with hat)"), + (("ÃĢ", "ÃĢ", "e"), "non-gsm Welsh char (e with dots)"), + # (("Ò", "Ò", "O"), "non-gsm Welsh char (capital O with grave accent)"), # conflicts with Vietnamese + (("í", "í", "i"), "non-gsm Welsh char (i with accent)"), +) + + +@pytest.mark.parametrize("char, expected_sms, expected_ascii", params, ids=ids) +def test_encode_chars_different_between_ascii_and_sms( + char, expected_sms, expected_ascii +): + assert SanitiseSMS.encode_char(char) == expected_sms + assert SanitiseASCII.encode_char(char) == expected_ascii + + +@pytest.mark.parametrize( + "codepoint, char", + [ + ("0041", "A"), + ("0061", "a"), + ], +) +def test_get_unicode_char_from_codepoint(codepoint, char): + assert SanitiseText.get_unicode_char_from_codepoint(codepoint) == char + + +@pytest.mark.parametrize( + "bad_input", ["", "GJ", "00001", '0001";import sys;sys.exit(0)"'] +) +def test_get_unicode_char_from_codepoint_rejects_bad_input(bad_input): + with pytest.raises(ValueError): + SanitiseText.get_unicode_char_from_codepoint(bad_input) + + +@pytest.mark.parametrize( + "content, expected", + [ + ("ŁōdÅē", "?odz"), + ( + "The quick brown fox jumps over the lazy dog", + "The quick brown fox jumps over the lazy dog", + ), + ], +) +def test_encode_string(content, expected): + assert SanitiseSMS.encode(content) == expected + assert SanitiseASCII.encode(content) == expected + + +@pytest.mark.parametrize( + "content, cls, expected", + [ + ("The quick brown fox jumps over the lazy dog", SanitiseSMS, set()), + ( + "The “quick” brown fox has some downgradable characters\xa0", + SanitiseSMS, + set(), + ), + ("Need more 🐮🔔", SanitiseSMS, {"🐮", "🔔"}), + ("Å´ÃĒlsh chÃĸrÃĸctÃĒrs ÃĸrÃĒ cômpÃĸtÃŽblÃĒ wÃŽth SanitiseSMS", SanitiseSMS, set()), + ("Lots of GSM chars that arent ascii compatible:\n\râ‚Ŧ", SanitiseSMS, set()), + ( + "Lots of GSM chars that arent ascii compatible:\n\râ‚Ŧ", + SanitiseASCII, + {"\n", "\r", "â‚Ŧ"}, + ), + ("Î‘Ī…Ī„ĪŒ ÎĩίÎŊιΚ έÎŊÎą Ī„ÎĩĪƒĪ„", SanitiseSMS, set()), + ("。、“”()īŧš;īŧŸīŧ", SanitiseSMS, set()), # Chinese punctuation + ], +) +def test_sms_encoding_get_non_compatible_characters(content, cls, expected): + assert cls.get_non_compatible_characters(content) == expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("ė´ę˛ƒė€ í…ŒėŠ¤íŠ¸ėž…ë‹ˆë‹¤", True), # Korean + ("Î‘Ī…Ī„ĪŒ ÎĩίÎŊιΚ έÎŊÎą Ī„ÎĩĪƒĪ„", True), # Greek + ("Đ­Ņ‚Đž ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēа", True), # Russian + ("⏙ā¸ĩāšˆā¸„ā¸ˇā¸­ā¸ā¸˛ā¸Ŗā¸—ā¸”ā¸Ē⏭⏚", True), # Thai + ("āŽ‡āŽ¤ā¯ āŽ’āŽ°ā¯ āŽšā¯‡āŽžāŽ¤āŽŠā¯ˆ", True), # Tamil + ("これはテ゚トです", True), # Japanese + ("ĐÃĸy là máģ™t bài kiáģƒm tra", True), # Vietnamese + ("𐤓𐤓𐤓𐤈𐤆", False), # Phoenician + ("čŋ™æ˜¯ä¸€æŦĄæĩ‹č¯•", True), # Mandarin (Simplified) + ("Bunda TÃŧrkçe karakterler var", True), # Turkish + ( + "į›žį‰Œé•å¸æ˜¯įŦŦä¸€į§é‡‡į”¨į™Ŋ铜åˆļäŊœįš„5įžŽåˆ†įĄŦ币īŧŒį”ąčŠšå§†æ–¯ÂˇBÂˇæœ—åŸƒå…‹čŽžčŽĄīŧŒäģŽ1866åš´å‘čĄŒåˆ°1883åš´å†į”ąč‡Ēį”ąåĨŗįĨžå¤´åƒé•å¸å–äģŖã€‚", + True, + ), # Chinese from wikipedia 1 + ( + "å›Ŋ际åŋ—æ„ŋ者æ—Ĩį‚翝åš´įš„12月5æ—ĨīŧŒåŽƒæ˜¯į”ąč”åˆå›Ŋ大äŧšåœ¨1985åš´12月17æ—Ĩ通čŋ‡įš„A/RES/40/212å†ŗčŽŽ[1]ä¸ŠįĄŽåŽšįš„[2]。", + True, + ), # Chinese from wikipedia 2 + ( + "å“Ēä¸€į¨Žå¤šé‚ŠåŊĸå…§éƒ¨č‡ŗå°‘å­˜åœ¨ä¸€å€‹å¯äģĨįœ‹čĻ‹å¤šé‚ŠåŊĸæ‰€æœ‰é‚Šį•Œå’Œæ‰€æœ‰å…§éƒ¨å€åŸŸįš„éģžīŧŸ", + True, + ), # Chinese from wikipedia 3 + ( + """éƒŊæŸæž—åœ¨åŽ˜æ–šåŸŽå¸‚é‚Šį•Œå…§įš„äēēåŖæ˜¯å¤§į´„495,000äēēīŧˆæ„›įˆžč˜­ä¸­å¤Žįĩąč¨ˆč™•2002åš´äēēåŖčĒŋæŸĨīŧ‰īŧŒ + į„ļč€Œé€™į¨Žįĩąč¨ˆåˇ˛į˛’有äģ€éēŧå¤Ēå¤§įš„æ„įžŠīŧŒå› į‚ēéƒŊæŸæž—įš„å¸‚éƒŠåœ°å€å’ŒčĄ›æ˜ŸåŸŽéŽŽåˇ˛įļ“大嚅地į™ŧåą•čˆ‡æ“´åŧĩ。""", + True, + ), # Chinese from wikipedia 4 + ( + "一名是Dubh Linnīŧˆæ„›įˆžč˜­čĒžīŧŒæ„į‚ē「éģ‘č‰˛įš„æ°´æą ã€īŧ‰įš„č‹ąåœ‹įŋ’čĒžã€‚į•ļį„ļ䚟有äēēčŗĒᖑ這čĒžæēã€‚", + True, + ), # Chinese from wikipedia 5 + ( + "éƒŊ柏林æ‹Ĩæœ‰ä¸–į•Œé—ģåįš„æ–‡å­ĻåŽ†å˛īŧŒæ›žįģäē§į”Ÿčŋ‡čŽ¸å¤šæ°å‡ēįš„æ–‡å­ĻåŽļīŧŒäž‹åĻ‚č¯ēč´å°”æ–‡å­ĻåĨ–åž—ä¸ģ威åģ‰Âˇåˇ´į‰šå‹’¡åļčŠã€č•­äŧ¯į´å’ŒåĄžįš†įˆžÂˇč˛å…‹į‰šã€‚", + True, + ), # Chinese from wikipedia 6 + ( + "æ„›įˆžč˜­åœ‹åŽļåšį‰Šé¤¨įš„å››ä¸Ē分éĻ†ä¸­æœ‰ä¸‰å€‹åˆ†é¤¨éƒŊäŊæ–ŧéƒŊ柏林īŧšč€ƒå¤å­Ļ分éĻ†åœ¨åŸē尔äģŖå°”襗īŧŒčŖ…éĨ°č‰ēæœ¯å’ŒåŽ†å˛åˆ†éĻ†åœ¨æŸ¯æž—æ–¯å†›čĨīŧŒč€Œč‡Ēį„ļå˛åˆ†éĻ†åœ¨æĸ…æž—čĄ—[12]。", + True, + ), # Chinese from wikipedia 7 + ( + "åžž17䏖ᴀ開始īŧŒåŸŽå¸‚在å¯Ŧ闊街道äē‹å‹™å§”å“Ąæœƒįš„åšĢ劊下開始čŋ…速擴åŧĩ。䚔æ˛ģäēšéƒŊ柏林曞一åēĻæ˜¯å¤§č‹ąå¸åœ‹åƒ…æŦĄæ–ŧå€Ģæ•Ļįš„įŦŦäēŒå¤§åŸŽå¸‚。", + True, + ), # Chinese from wikipedia 8 + ( + "一äē›č‘—åįš„éƒŊæŸæž—čĄ—é“åģēᝉäģäģĨ倒閉前在此įļ“į‡Ÿįš„é…’å§å’Œå•†æĨ­å…Ŧ司å‘Ŋ名。", + True, + ), # Chinese from wikipedia 9 + ( + "1922åš´īŧŒéš¨č‘—æ„›įˆžč˜­įš„åˆ†čŖ‚īŧŒéƒŊ柏林成į‚ēæ„›įˆžč˜­č‡Ēį”ąé‚Ļīŧˆ1922嚴–1937åš´īŧ‰įš„éĻ–éƒŊã€‚įžåœ¨å‰‡į‚ēæ„›įˆžč˜­å…ąå’Œåœ‹įš„éĻ–éƒŊ。", + True, + ), # Chinese from wikipedia 10 + ( + """Dưáģ›i đÃĸy là danh sÃĄch táēĨt cáēŖ cÃĄc tÃĒn ngưáģi dÚng hiáģ‡n đang cÃŗ + táēĄi Wikipedia, hoáēˇc nháģ¯ng tÃĒn ngưáģi dÚng trong máģ™t nhÃŗm cháģ‰ Ä‘áģ‹nh. """, + True, + ), # Vietnamese from wikipedia 1 + ( + """CÃĄc báēŖo quáēŖn viÃĒn đáēŖm nháē­n nháģ¯ng trÃĄch nhiáģ‡m này váģ›i tư cÃĄch là tÃŦnh + nguyáģ‡n viÃĒn sau khi tráēŖi qua quÃĄ trÃŦnh xem xÊt cáģ§a cáģ™ng đáģ“ng. """, + True, + ), # Vietnamese from wikipedia 2 + ( + """Háģ không bao giáģ Ä‘Æ°áģŖc yÃĒu cáē§u sáģ­ dáģĨng cÃĄc công cáģĨ cáģ§a mÃŦnh và không bao + giáģ Ä‘Æ°áģŖc sáģ­ dáģĨng chÃēng đáģƒ giành láģŖi tháēŋ trong máģ™t cuáģ™c tranh cháēĨp mà háģ cÃŗ + tham gia. Không nÃĒn nháē§m láēĢn báēŖo quáēŖn viÃĒn váģ›i quáēŖn tráģ‹ viÃĒn háģ‡ + tháģ‘ng cáģ§a Wikimedia ("sysadmins").""", + True, + ), # Vietnamese from wikipedia 3 + ( + "Đáģƒ Ä‘áēĄt đưáģŖc máģĨc tiÃĒu chung Ä‘Ãŗ, Wikipedia đáģ ra máģ™t sáģ‘ quy đáģ‹nh và hưáģ›ng dáēĢn. ", + True, + ), # Vietnamese from wikipedia 4 + ("Wikipedia là máģ™t bÃĄch khoa toàn thư. ", True), # Vietnamese from wikipedia 5 + ( + "PháēŖi đáēŖm báēŖo bài viáēŋt mang láēĄi ích láģŖi cho đáģ™c giáēŖ (coi đáģ™c giáēŖ là yáēŋu táģ‘ quan tráģng khi viáēŋt bài)", + True, + ), # Vietnamese from wikipedia 6 + ( + """Bài viáēŋt áģŸ Wikipedia cÃŗ tháģƒ cháģŠa đáģąng táģĢ ngáģ¯ và hÃŦnh áēŖnh gÃĸy khÃŗ cháģ‹u + nhưng cháģ‰ vÃŦ máģĨc đích táģ‘t đáēšp. Không cáē§n thÃĒm vào pháģ§ Ä‘áģ‹nh trÃĄch nhiáģ‡m.""", + True, + ), # Vietnamese from wikipedia 7 + ( + "ĐáģĢng sáģ­ dáģĨng hÃŦnh áēŖnh mà cháģ‰ cÃŗ tháģƒ xem đưáģŖc chính xÃĄc váģ›i công cáģĨ 3D.", + True, + ), # Vietnamese from wikipedia 8 + ( + """Trích dáēĢn báēĨt cáģŠ nôi dung tranh luáē­n gáģ‘c nào cÅŠng nÃĒn cÃŗ liÃĒn quan + đáēŋn tranh luáē­n Ä‘Ãŗ (hoáēˇc minh háģa cho phong cÃĄch) và cháģ‰ nÃĒn dài váģĢa đáģ§.""", + True, + ), # Vietnamese from wikipedia 9 + ( + """Không tung tin váģ‹t, thông tin sai láģ‡ch hoáēˇc náģ™i dung không kiáģƒm cháģŠng đưáģŖc vào bài viáēŋt. + Tuy nhiÃĒn, nháģ¯ng bài viáēŋt váģ nháģ¯ng tin váģ‹t náģ•i báē­t đưáģŖc cháēĨp nháē­n.""", + True, + ), # Vietnamese from wikipedia 10 + ( + "ėˆ˜ëĄë˜ė–´ ėžˆėœŧ늰, 넘겨ėŖŧ기ëĨŧ íŦ함한 ėŧ반 ëŦ¸ė„œ ėˆ˜ëŠ” 1,434,776ę°œã€‚", + True, + ), # Korean from wikipedia includes circle-period + ( + "æ—ĨæœŦčĒžčĄ¨č¨˜ãĢも寞åŋœã™ã‚‹ã‚ˆã†ãĢãĒり[1]、垐々ãĢæ—ĨæœŦäēēぎãƒĻãƒŧã‚ļãƒŧもåĸ—大しãĻã„ãŖãŸã€ã¨čŋ°ãšã‚‰ã‚ŒãĻいる。", + True, + ), # Japanese from wikipedia includes circle-period + ( + "DSHS:我äģŦå‘įŽ°æ‚¨įš„č´Ļæˆˇå­˜åœ¨æŊœåœ¨æŦēč¯ˆčĄŒä¸ēã€‚č¯ˇč‡´į”ĩæ‚¨įš„ EBT åĄčƒŒéĸįš„åˇį å矿­ĸ或前垀åŊ“地办å…ŦåŽ¤čŽˇå–ä¸€ä¸Ē新č´Ļæˆˇã€‚å›žå¤ “STOP(退čŽĸ)” 退čŽĸ", + True, + ), # State of Washington Chinese Simplified + ( + """DSHS៖ ប ើងោនកត់សម្គážļ ល់ប ើញក្ដរបោកប្រោស់ជážļសក្ដážļ នážģពលបៅបលើគណនីរបស់ážĸ្នក។ សážŧមបៅបៅបលខ #បៅបលើខនងក្ដត + EBT របស់ážĸ្នក ប ើមបីបោោះបង់ ážŦក៏បៅក្ដន់ក្ដរយážļិ ល័ បៅកនážģងតំបន់របស់ážĸ្នក + ប ើមបីបសនើសážģំក្ដតថ្មី។ ប្លើ តបជážļážĸ្កសរ ឈប់ ប ើមបីបញ្ឈប់""", + True, + ), # State of Washington Khmer + ( + """DSHS: ęˇ€í•˜ė˜ ęŗ„ė • ėƒė— ė‚Ŧ기가 ėŧė–´ë‚Ŧė„ 가ëŠĨė„ąė´ íŦė°Šë˜ė—ˆėŠĩ니다. ęˇ€í•˜ė˜ EBT ėš´ë“œ ë’ˇëŠ´ė—ėžˆëŠ” + 번호로 ė „í™”ëĨŧ ęą¸ė–´ ėˇ¨ė†Œí•˜ęą°ë‚˜ í˜„ė§€ ė‚ŦëŦ´ė†ŒëĄœ ę°€ė„œ 냈 ę˛ƒė„ 발급 받ėœŧė„¸ėš”. ė¤‘ë‹¨í•˜ë ¤ëŠ´ė¤‘ë‹¨ė´ëŧęŗ  íšŒė‹ í•˜ė„¸ėš”.""", + True, + ), # State of WA Korean + ( + """āē‚ āē„āģāģ‰ āē§āē˛āēĄāēāē˛āē™āēĒāģāģ‰āģ‚āēāē‡āē—āē­āē˛āē”āģ€āē›āēąāē™āģ„āē›āģ„āē” āģāģ‰ DSHS: āēžāē§āēāģ€āēŽāē˛āēģāģ„āē”āēĒāģāģ‰āē‡āēąāģ€āēāē”āģ€āēĢāē™āēąāēāē˛āē™āēĒāģ‚āēāē‡āē—āēĩāģˆāē­āē˛āē”āģ€āē›āēąāē™āģ„āē›āģ„āē”āģƒāģāģ‰āē™āēšāē™āēąāēŠāē‚ āē­āē‡āē—āēĩāģˆāē˛āē™. + āģ‚āē—āēĢāē˛ # āē— āēĸāēĩāģˆ āē”āē˛āģāģ‰āē™āēĢ āē‡āēąāē‚āē­āē‡āēšāē”āēą EBT āē‚āē­āē‡āē—āēĩāģˆāē˛āē™āģ€āēžāē­āēāēāēģ āģ€āēĨāē āēĢ āģ„āē›āēāē‡āēąāēĢāē­āģāģ‰āē‡āēāē˛āē™āē›āē°āēˆāē˛ āē—āē­āģāģ‰āē‡āē– āē™āē‚āē­āē‡āē—āēĩāģˆāē˛āē™ āģ€āēžāēĩāģˆāē­āē‚ + āēšāē”āēą āģƒāēĢāēĄāēĩāģˆ . āē•āē­āēšāēāēšāēąāē”āē§āģāģ‰ āē STOP (āēĸāē¸āē”āģ€āēŠāē˛āēģ) āģ€āēžāē­āēĸāē¸āē”āģ€āēŠāē˛āēģ""", + True, + ), # State of WA Lao + ( + """Fariin Khiyaamo Suurtogal ah DSHS: Waxaanu ka ogaanay khiyaamo suurtogal ah akoonkaaga. + Wax # ee ku yaal xaga danbe ee kadadhka + EBT si aad u joojisid ama u aadid xafiiska deegaanka uguna dalbatid a new one (mid cusub). + Ku jawaab JOOJI si aad u joojisid""", + True, + ), # State of WA Somali + ( + "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØŽØ¯Ų…Ø§ØĒ Ø§Ų„Ø§ØŦØĒŲ…Ø§ØšŲŠØŠ ŲˆØ§Ų„ØĩØ­ŲŠØŠ ؁؊ ŲˆŲ„Ø§ŲŠØŠ ŲˆØ§Ø´Ų†ØˇŲ† (Washington State Department of Social and Health Services, WA DSHS): ØŗØĒŲØŦØąŲ‰ Ø§Ų„Ų…Ų‚Ø§Ø¨Ų„ØŠ Ø§Ų„Ų‡Ø§ØĒŲŲŠØŠ Ų…ØšŲƒ Ø§Ų„Ų…ØšŲ†ŲŠØŠ Ø¨Ų…ØąØ§Ų‚Ø¨ØŠ ØŦŲˆØ¯ØŠ Ø§Ų„ØˇØšØ§Ų… ŲŠŲˆŲ… xx/xx/xx Ø§Ų„ØŗØ§ØšØŠ 00:00 ØĩØ¨Ø§Ø­Ų‹Ø§/Ų…ØŗØ§ØĄŲ‹. Ų‚Ø¯ ŲŠØ¤Ø¯ŲŠ Ø§Ų„ŲØ´Ų„ ØĨŲ„Ų‰ ØĨØēŲ„Ø§Ų‚ Ų…ØŽØĩØĩاØĒ؃. اØĒØĩŲ„ Ø¨Ø§Ų„ØąŲ‚Ų… 1-800-473-5661 ØĨذا ŲƒØ§Ų†ØĒ Ų„Ø¯ŲŠŲƒ ØŖØŗØĻŲ„ØŠ.", # noqa + True, + ), # State of WA Arabic + ( + "WA DSHS: ā¨¤āŠā¨šā¨žā¨ĄāŠ€ ā¨—āŠā¨Ŗā¨ĩāŠąā¨¤ā¨ž ⍍ā¨ŋā¨¯āŠ°ā¨¤ā¨°ā¨Ŗ ā¨­āŠ‹ā¨œā¨¨ ā¨Ģā¨ŧāŠ‹ā¨¨ ā¨‡āŠ°ā¨Ÿā¨°ā¨ĩā¨ŋ⍊ xx/xx/xx 'ā¨¤āŠ‡ ⍏ā¨ĩāŠ‡ā¨°āŠ‡ 00:00 ā¨ĩā¨œāŠ‡/⍏ā¨ŧā¨žā¨Ž 'ā¨¤āŠ‡ ā¨šāŠˆāĨ¤ ⍅⍏ā¨Ģā¨˛ā¨¤ā¨ž ā¨¤āŠā¨šā¨žā¨ĄāŠ‡ ā¨˛ā¨žā¨­ā¨žā¨‚ ā¨¨āŠ‚āŠ° ā¨ŦāŠ°ā¨Ļ ⍕⍰⍍ ā¨Ļā¨ž ā¨•ā¨žā¨°ā¨¨ ā¨Ŧ⍪ ⍏⍕ā¨ĻāŠ€ ā¨šāŠˆāĨ¤ ⍏ā¨ĩā¨žā¨˛ā¨žā¨‚ ā¨¨ā¨žā¨˛ 1-800-473-5661 'ā¨¤āŠ‡ ā¨•ā¨žā¨˛ ā¨•ā¨°āŠ‹āĨ¤", # noqa + True, + ), # State of WA Punjabi + ( + "WA DSHS: ā¨ĩā¨ŋā¨…ā¨•ā¨¤āŠ€ā¨—ā¨¤ ā¨­āŠ‹ā¨œā¨¨ ā¨ĩā¨ŋāŠąā¨š ā¨¤āŠā¨šā¨žā¨Ąā¨ž ā¨—āŠā¨Ŗā¨ĩāŠąā¨¤ā¨ž ⍍ā¨ŋā¨¯āŠ°ā¨¤ā¨°ā¨Ŗ ā¨‡āŠ°ā¨Ÿā¨°ā¨ĩā¨ŋ⍊ xx/xx/xx 'ā¨¤āŠ‡ ⍏ā¨ĩāŠ‡ā¨°āŠ‡ 00:00 ā¨ĩā¨œāŠ‡ /⍏ā¨ŧā¨žā¨Ž 00:00 ā¨ĩā¨œāŠ‡ ā¨šāŠˆāĨ¤ ⍅⍏ā¨Ģā¨˛ā¨¤ā¨ž ā¨¤āŠā¨šā¨žā¨ĄāŠ‡ ā¨˛ā¨žā¨­ā¨žā¨‚ ā¨¨āŠ‚āŠ° ā¨ŦāŠ°ā¨Ļ ⍕⍰⍍ ā¨Ļā¨ž ā¨•ā¨žā¨°ā¨¨ ā¨Ŧ⍪ ⍏⍕ā¨ĻāŠ€ ā¨šāŠˆāĨ¤ 1-800-473-5661 'ā¨¤āŠ‡ w/⍏ā¨ĩā¨žā¨˛ ā¨¨ā¨žā¨˛ ā¨•ā¨žā¨˛ ā¨•ā¨°āŠ‹āĨ¤", # noqa + True, + ), # State of WA Punjabi + ], +) +def test_sms_supporting_additional_languages(content, expected): + assert SanitiseSMS.is_extended_language(content) is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("ė´ę˛ƒė€ í…ŒėŠ¤íŠ¸ėž…ë‹ˆë‹¤", set()), # Korean + ("Î‘Ī…Ī„ĪŒ ÎĩίÎŊιΚ έÎŊÎą Ī„ÎĩĪƒĪ„", set()), # Greek + ("Đ­Ņ‚Đž ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēа", set()), # Russian + ("⏙ā¸ĩāšˆā¸„ā¸ˇā¸­ā¸ā¸˛ā¸Ŗā¸—ā¸”ā¸Ē⏭⏚", set()), # Thai + ("āŽ‡āŽ¤ā¯ āŽ’āŽ°ā¯ āŽšā¯‡āŽžāŽ¤āŽŠā¯ˆ", set()), # Tamil + ("これはテ゚トです", set()), # Japanese + ("ĐÃĸy là máģ™t bài kiáģƒm tra", set()), # Vietnamese + ("𐤓𐤓𐤓𐤈𐤆", {"𐤆", "𐤈", "𐤓"}), # Phoenician + ("čŋ™æ˜¯ä¸€æŦĄæĩ‹č¯•", set()), # Mandarin (Simplified) + ("Bunda TÃŧrkçe karakterler var", set()), # Turkish + ("。、“”()īŧš;īŧŸīŧ", set()), # Chinese punctuation + (" Ų Ų", set()), # Arabic diacritics + ( + "WA DSHS: ā¨¤āŠā¨šā¨žā¨ĄāŠ€ ā¨—āŠā¨Ŗā¨ĩāŠąā¨¤ā¨ž ⍍ā¨ŋā¨¯āŠ°ā¨¤ā¨°ā¨Ŗ ā¨­āŠ‹ā¨œā¨¨ ā¨Ģā¨ŧāŠ‹ā¨¨ ā¨‡āŠ°ā¨Ÿā¨°ā¨ĩā¨ŋ⍊ xx/xx/xx 'ā¨¤āŠ‡ ⍏ā¨ĩāŠ‡ā¨°āŠ‡ 00:00 ā¨ĩā¨œāŠ‡/⍏ā¨ŧā¨žā¨Ž 'ā¨¤āŠ‡ ā¨šāŠˆāĨ¤ ⍅⍏ā¨Ģā¨˛ā¨¤ā¨ž ā¨¤āŠā¨šā¨žā¨ĄāŠ‡ ā¨˛ā¨žā¨­ā¨žā¨‚ ā¨¨āŠ‚āŠ° ā¨ŦāŠ°ā¨Ļ ⍕⍰⍍ ā¨Ļā¨ž ā¨•ā¨žā¨°ā¨¨ ā¨Ŧ⍪ ⍏⍕ā¨ĻāŠ€ ā¨šāŠˆāĨ¤ ⍏ā¨ĩā¨žā¨˛ā¨žā¨‚ ā¨¨ā¨žā¨˛ 1-800-473-5661 'ā¨¤āŠ‡ ā¨•ā¨žā¨˛ ā¨•ā¨°āŠ‹āĨ¤", # noqa + set(), + ), # Punjabi + ( + "WA DSHS: ā¨ĩā¨ŋā¨…ā¨•ā¨¤āŠ€ā¨—ā¨¤ ā¨­āŠ‹ā¨œā¨¨ ā¨ĩā¨ŋāŠąā¨š ā¨¤āŠā¨šā¨žā¨Ąā¨ž ā¨—āŠā¨Ŗā¨ĩāŠąā¨¤ā¨ž ⍍ā¨ŋā¨¯āŠ°ā¨¤ā¨°ā¨Ŗ ā¨‡āŠ°ā¨Ÿā¨°ā¨ĩā¨ŋ⍊ xx/xx/xx 'ā¨¤āŠ‡ ⍏ā¨ĩāŠ‡ā¨°āŠ‡ 00:00 ā¨ĩā¨œāŠ‡ /⍏ā¨ŧā¨žā¨Ž 00:00 ā¨ĩā¨œāŠ‡ ā¨šāŠˆāĨ¤ ⍅⍏ā¨Ģā¨˛ā¨¤ā¨ž ā¨¤āŠā¨šā¨žā¨ĄāŠ‡ ā¨˛ā¨žā¨­ā¨žā¨‚ ā¨¨āŠ‚āŠ° ā¨ŦāŠ°ā¨Ļ ⍕⍰⍍ ā¨Ļā¨ž ā¨•ā¨žā¨°ā¨¨ ā¨Ŧ⍪ ⍏⍕ā¨ĻāŠ€ ā¨šāŠˆāĨ¤ 1-800-473-5661 'ā¨¤āŠ‡ w/⍏ā¨ĩā¨žā¨˛ ā¨¨ā¨žā¨˛ ā¨•ā¨žā¨˛ ā¨•ā¨°āŠ‹āĨ¤", # noqa + set(), + ), # more Punjabi + ], +) +def test_get_non_compatible_characters(content, expected): + assert SanitiseSMS.get_non_compatible_characters(content) == expected diff --git a/tests/notification_utils/test_serialised_model.py b/tests/notification_utils/test_serialised_model.py new file mode 100644 index 000000000..b83a4d1e5 --- /dev/null +++ b/tests/notification_utils/test_serialised_model.py @@ -0,0 +1,220 @@ +import sys + +import pytest + +from notifications_utils.serialised_model import ( + SerialisedModel, + SerialisedModelCollection, +) + + +def test_cant_be_instatiated_with_abstract_properties(): + class Custom(SerialisedModel): + pass + + class CustomCollection(SerialisedModelCollection): + pass + + with pytest.raises(TypeError) as e: + SerialisedModel() + + if sys.version_info < (3, 9): + assert str(e.value) == ( + "Can't instantiate abstract class SerialisedModel with abstract methods ALLOWED_PROPERTIES" + ) + else: + assert "Can't instantiate abstract class SerialisedModel with abstract method ALLOWED_PROPERTIES" + + with pytest.raises(TypeError) as e: + Custom() + + if sys.version_info < (3, 9): + assert str(e.value) == ( + "Can't instantiate abstract class Custom with abstract methods ALLOWED_PROPERTIES" + ) + else: + assert str(e.value) == ( + "Can't instantiate abstract class Custom without an implementation for abstract method 'ALLOWED_PROPERTIES'" + ) + + with pytest.raises(TypeError) as e: + SerialisedModelCollection() + + if sys.version_info < (3, 9): + assert str(e.value) == ( + "Can't instantiate abstract class SerialisedModelCollection with abstract methods model" + ) + else: + assert str(e.value).startswith( + "Can't instantiate abstract class SerialisedModelCollection without an implementation" + ) + + with pytest.raises(TypeError) as e: + CustomCollection() + + if sys.version_info < (3, 9): + assert str(e.value) == ( + "Can't instantiate abstract class CustomCollection with abstract methods model" + ) + else: + assert str(e.value) == ( + "Can't instantiate abstract class CustomCollection without an implementation for abstract method 'model'" + ) + + +def test_looks_up_from_dict(): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = {"foo"} + + assert Custom({"foo": "bar"}).foo == "bar" + + +def test_cant_override_custom_property_from_dict(): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = {"foo"} + + @property + def foo(self): + return "bar" + + with pytest.raises(AttributeError) as e: + assert Custom({"foo": "NOPE"}).foo == "bar" + assert ( + str(e.value) + == "property 'foo' of 'test_cant_override_custom_property_from_dict..Custom' object has no setter" + ) + + +@pytest.mark.parametrize( + "json_response", + ( + {}, + {"foo": "bar"}, # Should still raise an exception + ), +) +def test_model_raises_for_unknown_attributes(json_response): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = set() + + model = Custom(json_response) + + assert model.ALLOWED_PROPERTIES == set() + + with pytest.raises(AttributeError) as e: + model.foo + + assert str(e.value) == ("'Custom' object has no attribute 'foo'") + + +def test_model_raises_keyerror_if_item_missing_from_dict(): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = {"foo"} + + with pytest.raises(KeyError) as e: + Custom({}).foo + + assert str(e.value) == "'foo'" + + +@pytest.mark.parametrize( + "json_response", + ( + {}, + {"foo": "bar"}, # Should be ignored + ), +) +def test_model_doesnt_swallow_attribute_errors(json_response): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = set() + + @property + def foo(self): + raise AttributeError("Something has gone wrong") + + with pytest.raises(AttributeError) as e: + Custom(json_response).foo + + assert str(e.value) == "Something has gone wrong" + + +def test_dynamic_properties_are_introspectable(): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = {"foo", "bar", "baz"} + + instance = Custom({"foo": "", "bar": "", "baz": ""}) + + assert dir(instance)[-3:] == ["bar", "baz", "foo"] + + +def test_empty_serialised_model_collection(): + class CustomCollection(SerialisedModelCollection): + model = None + + instance = CustomCollection([]) + + assert not instance + assert len(instance) == 0 + + +def test_serialised_model_collection_returns_models_from_list(): + class Custom(SerialisedModel): + ALLOWED_PROPERTIES = {"x"} + + class CustomCollection(SerialisedModelCollection): + model = Custom + + instance = CustomCollection( + [ + {"x": "foo"}, + {"x": "bar"}, + {"x": "baz"}, + ] + ) + + assert instance + assert len(instance) == 3 + + assert instance[0].x == "foo" + assert instance[1].x == "bar" + assert instance[2].x == "baz" + + assert [item.x for item in instance] == [ + "foo", + "bar", + "baz", + ] + + assert [type(item) for item in instance + [1, 2, 3]] == [ + Custom, + Custom, + Custom, + int, + int, + int, + ] + + instance_2 = CustomCollection( + [ + {"x": "red"}, + {"x": "green"}, + {"x": "blue"}, + ] + ) + + assert [item.x for item in instance + instance_2] == [ + "foo", + "bar", + "baz", + "red", + "green", + "blue", + ] + + assert [item.x for item in instance_2 + instance] == [ + "red", + "green", + "blue", + "foo", + "bar", + "baz", + ] diff --git a/tests/notification_utils/test_take.py b/tests/notification_utils/test_take.py new file mode 100644 index 000000000..7ec5218c1 --- /dev/null +++ b/tests/notification_utils/test_take.py @@ -0,0 +1,19 @@ +from notifications_utils.take import Take + + +def _uppercase(value): + return value.upper() + + +def _append(value, to_append): + return value + to_append + + +def _prepend_with_service_name(value, service_name=None): + return "{}: {}".format(service_name, value) + + +def test_take(): + assert "Service name: HELLO WORLD!" == Take("hello world").then(_uppercase).then( + _append, "!" + ).then(_prepend_with_service_name, service_name="Service name") diff --git a/tests/notification_utils/test_template_change.py b/tests/notification_utils/test_template_change.py new file mode 100644 index 000000000..e0b8df892 --- /dev/null +++ b/tests/notification_utils/test_template_change.py @@ -0,0 +1,135 @@ +import pytest + +from notifications_utils.template_change import TemplateChange + +from .test_base_template import ConcreteTemplate + + +@pytest.mark.parametrize( + "old_template, new_template, should_differ", + [ + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + False, + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((3)) ((2)) ((1))"}), + False, + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((1)) ((2)) ((2)) ((3)) ((3))"}), + False, + ), + ( + ConcreteTemplate({"content": "((1))"}), + ConcreteTemplate({"content": "((1)) ((2))"}), + True, + ), + ( + ConcreteTemplate({"content": "((1)) ((2))"}), + ConcreteTemplate({"content": "((1))"}), + True, + ), + ( + ConcreteTemplate({"content": "((a)) ((b))"}), + ConcreteTemplate({"content": "((A)) (( B_ ))"}), + False, + ), + ], +) +def test_checking_for_difference_between_templates( + old_template, new_template, should_differ +): + assert ( + TemplateChange(old_template, new_template).has_different_placeholders + == should_differ + ) + + +@pytest.mark.parametrize( + "old_template, new_template, placeholders_added", + [ + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((1)) ((2)) ((2)) ((3)) ((3))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1))"}), + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + set(["2", "3"]), + ), + ( + ConcreteTemplate({"content": "((a))"}), + ConcreteTemplate({"content": "((A)) ((B)) ((C))"}), + set(["B", "C"]), + ), + ], +) +def test_placeholders_added(old_template, new_template, placeholders_added): + assert ( + TemplateChange(old_template, new_template).placeholders_added + == placeholders_added + ) + + +@pytest.mark.parametrize( + "old_template, new_template, placeholders_removed", + [ + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1)) ((1)) ((2)) ((2)) ((3)) ((3))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1))"}), + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + set(), + ), + ( + ConcreteTemplate({"content": "((1)) ((2)) ((3))"}), + ConcreteTemplate({"content": "((1))"}), + set(["2", "3"]), + ), + ( + ConcreteTemplate({"content": "((a)) ((b)) ((c))"}), + ConcreteTemplate({"content": "((A))"}), + set(["b", "c"]), + ), + ], +) +def test_placeholders_removed(old_template, new_template, placeholders_removed): + assert ( + TemplateChange(old_template, new_template).placeholders_removed + == placeholders_removed + ) + + +def test_ordering_of_placeholders_is_preserved(): + before = ConcreteTemplate({"content": "((dog)) ((cat)) ((rat))"}) + after = ConcreteTemplate({"content": "((platypus)) ((echidna)) ((quokka))"}) + change = TemplateChange(before, after) + assert change.placeholders_removed == ["dog", "cat", "rat"] == before.placeholders + assert ( + change.placeholders_added + == ["platypus", "echidna", "quokka"] + == after.placeholders + ) diff --git a/tests/notification_utils/test_template_types.py b/tests/notification_utils/test_template_types.py new file mode 100644 index 000000000..1209d4dc9 --- /dev/null +++ b/tests/notification_utils/test_template_types.py @@ -0,0 +1,3388 @@ +import os +import sys +from functools import partial +from time import process_time +from unittest import mock + +import pytest +from bs4 import BeautifulSoup +from freezegun import freeze_time +from markupsafe import Markup +from ordered_set import OrderedSet + +from notifications_utils.formatters import unlink_govuk_escaped +from notifications_utils.template import ( + BaseBroadcastTemplate, + BaseEmailTemplate, + BaseLetterTemplate, + BroadcastMessageTemplate, + BroadcastPreviewTemplate, + EmailPreviewTemplate, + HTMLEmailTemplate, + LetterImageTemplate, + LetterPreviewTemplate, + LetterPrintTemplate, + PlainTextEmailTemplate, + SMSBodyPreviewTemplate, + SMSMessageTemplate, + SMSPreviewTemplate, + SubjectMixin, + Template, +) + + +@pytest.mark.parametrize( + "template_class, expected_error", + ( + pytest.param( + Template, + ("Can't instantiate abstract class Template with abstract method __str__"), + marks=pytest.mark.skipif( + sys.version_info >= (3, 9), reason="‘methods’ will be singular" + ), + ), + pytest.param( + Template, + ( + "Can't instantiate abstract class Template without an implementation for abstract method '__str__'" + ), + marks=pytest.mark.skipif( + sys.version_info < (3, 9), reason="‘method’ will be pluralised" + ), + ), + pytest.param( + BaseEmailTemplate, + ( + "Can't instantiate abstract class BaseEmailTemplate with abstract methods __str__" + ), + marks=pytest.mark.skipif( + sys.version_info >= (3, 9), reason="‘methods’ will be singular" + ), + ), + pytest.param( + BaseEmailTemplate, + ( + "Can't instantiate abstract class BaseEmailTemplate without an implementation for abstract method" + ), + marks=pytest.mark.skipif( + sys.version_info < (3, 9), reason="‘method’ will be pluralised" + ), + ), + pytest.param( + BaseLetterTemplate, + ( + "Can't instantiate abstract class BaseLetterTemplate with abstract methods __str__" + ), + marks=pytest.mark.skipif( + sys.version_info >= (3, 9), reason="‘methods’ will be singular" + ), + ), + pytest.param( + BaseLetterTemplate, + ( + "Can't instantiate abstract class BaseLetterTemplate without an implementation for abstract method" + ), + marks=pytest.mark.skipif( + sys.version_info < (3, 9), reason="‘method’ will be pluralised" + ), + ), + pytest.param( + BaseBroadcastTemplate, + ( + "Can't instantiate abstract class BaseBroadcastTemplate with abstract methods __str__" + ), + marks=pytest.mark.skipif( + sys.version_info >= (3, 9), reason="‘methods’ will be singular" + ), + ), + pytest.param( + BaseBroadcastTemplate, + ( + "Can't instantiate abstract class BaseBroadcastTemplate without an implementation for abstract method" + ), + marks=pytest.mark.skipif( + sys.version_info < (3, 9), reason="‘method’ will be pluralised" + ), + ), + ), +) +def test_abstract_classes_cant_be_instantiated(template_class, expected_error): + with pytest.raises(TypeError) as error: + template_class({}) + # assert str(error.value) == expected_error + assert expected_error in str(error.value) + + +@pytest.mark.parametrize( + "template_class, expected_error", + ( + ( + HTMLEmailTemplate, + ("Cannot initialise HTMLEmailTemplate with sms template_type"), + ), + ( + LetterPreviewTemplate, + ("Cannot initialise LetterPreviewTemplate with sms template_type"), + ), + ( + BroadcastPreviewTemplate, + ("Cannot initialise BroadcastPreviewTemplate with sms template_type"), + ), + ), +) +def test_errors_for_incompatible_template_type(template_class, expected_error): + with pytest.raises(TypeError) as error: + template_class({"content": "", "subject": "", "template_type": "sms"}) + assert str(error.value) == expected_error + + +def test_html_email_inserts_body(): + assert "the <em>quick</em> brown fox" in str( + HTMLEmailTemplate( + { + "content": "the quick brown fox", + "subject": "", + "template_type": "email", + } + ) + ) + + +@pytest.mark.parametrize( + "content", ("DOCTYPE", "html", "body", "beta.notify.gov", "hello world") +) +def test_default_template(content): + assert content in str( + HTMLEmailTemplate( + { + "content": "hello world", + "subject": "", + "template_type": "email", + } + ) + ) + + +@pytest.mark.parametrize("show_banner", (True, False)) +def test_govuk_banner(show_banner): + email = HTMLEmailTemplate( + { + "content": "hello world", + "subject": "", + "template_type": "email", + } + ) + email.govuk_banner = show_banner + if show_banner: + assert "beta.notify.gov" in str(email) + else: + assert "beta.notify.gov" not in str(email) + + +def test_brand_banner_shows(): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + brand_banner=True, + govuk_banner=False, + ) + ) + assert ('') not in email + assert ( + 'role="presentation" width="100%" style="border-collapse: collapse;min-width: 100%;width: 100% !important;"' + ) in email + + +@pytest.mark.parametrize( + "brand_logo, brand_text, brand_colour", + [ + ("http://example.com/image.png", "Example", "red"), + ("http://example.com/image.png", "Example", "#f00"), + ("http://example.com/image.png", "Example", None), + ("http://example.com/image.png", "", "#f00"), + (None, "Example", "#f00"), + ], +) +def test_brand_data_shows(brand_logo, brand_text, brand_colour): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + brand_banner=True, + govuk_banner=False, + brand_logo=brand_logo, + brand_text=brand_text, + brand_colour=brand_colour, + ) + ) + + assert "GOV.UK" not in email + if brand_logo: + assert brand_logo in email + if brand_text: + assert brand_text in email + if brand_colour: + assert 'bgcolor="{}"'.format(brand_colour) in email + + +def test_alt_text_with_brand_text_and_govuk_banner_shown(): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + govuk_banner=True, + brand_logo="http://example.com/image.png", + brand_text="Example", + brand_banner=True, + brand_name="Notify Logo", + ) + ) + assert 'alt=""' in email + assert 'alt="Notify Logo"' not in email + + +def test_alt_text_with_no_brand_text_and_govuk_banner_shown(): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + govuk_banner=True, + brand_logo="http://example.com/image.png", + brand_text=None, + brand_banner=True, + brand_name="Notify Logo", + ) + ) + assert 'alt=""' not in email + assert 'alt="Notify Logo"' in email + + +@pytest.mark.parametrize( + "brand_banner, brand_text, expected_alt_text", + [ + (True, None, 'alt="Notify Logo"'), + (True, "Example", 'alt=""'), + (False, "Example", 'alt=""'), + (False, None, 'alt="Notify Logo"'), + ], +) +def test_alt_text_with_no_govuk_banner(brand_banner, brand_text, expected_alt_text): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + govuk_banner=False, + brand_logo="http://example.com/image.png", + brand_text=brand_text, + brand_banner=brand_banner, + brand_name="Notify Logo", + ) + ) + + assert expected_alt_text in email + + +@pytest.mark.parametrize("complete_html", (True, False)) +@pytest.mark.parametrize( + "branding_should_be_present, brand_logo, brand_text, brand_colour", + [ + (True, "http://example.com/image.png", "Example", "#f00"), + (True, "http://example.com/image.png", "Example", None), + (True, "http://example.com/image.png", "", None), + (False, None, "Example", "#f00"), + (False, "http://example.com/image.png", None, "#f00"), + ], +) +@pytest.mark.parametrize("content", ("DOCTYPE", "html", "body")) +def test_complete_html( + complete_html, + branding_should_be_present, + brand_logo, + brand_text, + brand_colour, + content, +): + email = str( + HTMLEmailTemplate( + {"content": "hello world", "subject": "", "template_type": "email"}, + complete_html=complete_html, + brand_logo=brand_logo, + brand_text=brand_text, + brand_colour=brand_colour, + ) + ) + + if complete_html: + assert content in email + else: + assert content not in email + + if branding_should_be_present: + assert brand_logo in email + assert brand_text in email + + if brand_colour: + assert brand_colour in email + assert "##" not in email + + +def test_subject_is_page_title(): + email = BeautifulSoup( + str( + HTMLEmailTemplate( + { + "content": "", + "subject": "this is the subject", + "template_type": "email", + }, + ) + ), + features="html.parser", + ) + assert email.select_one("title").text == "this is the subject" + + +def test_preheader_is_at_start_of_html_emails(): + assert ( + '\n' + "\n" + 'contentâ€Ļ' + ) in str( + HTMLEmailTemplate( + {"content": "content", "subject": "subject", "template_type": "email"} + ) + ) + + +@pytest.mark.parametrize( + "content, values, expected_preheader", + [ + ( + ( + "Hello (( name ))\n" + "\n" + '# This - is a "heading"\n' + "\n" + "My favourite websites' URLs are:\n" + "- GOV.UK\n" + "- https://www.example.com\n" + ), + {"name": "Jo"}, + "Hello Jo This – is a “heading” My favourite websites’ URLs are: â€ĸ GOV.​UK â€ĸ https://www.example.com", + ), + ( + ("[Markdown link](https://www.example.com)\n"), + {}, + "Markdown link", + ), + ( + """ + Lorem Ipsum is simply dummy text of the printing and + typesetting industry. + + Lorem Ipsum has been the industry’s standard dummy text + ever since the 1500s, when an unknown printer took a galley + of type and scrambled it to make a type specimen book. + + Lorem Ipsum is simply dummy text of the printing and + typesetting industry. + + Lorem Ipsum has been the industry’s standard dummy text + ever since the 1500s, when an unknown printer took a galley + of type and scrambled it to make a type specimen book. + """, + {}, + ( + "Lorem Ipsum is simply dummy text of the printing and " + "typesetting industry. Lorem Ipsum has been the industry’s " + "standard dummy text ever since the 1500s, when an unknown " + "printer took a galley of type and scrambled it to make a " + "type specimen book. Lorem Ipsu" + ), + ), + ( + "short email", + {}, + "short email", + ), + ], +) +@mock.patch( + "notifications_utils.template.HTMLEmailTemplate.jinja_template.render", + return_value="mocked", +) +def test_content_of_preheader_in_html_emails( + mock_jinja_template, + content, + values, + expected_preheader, +): + assert ( + str( + HTMLEmailTemplate( + {"content": content, "subject": "subject", "template_type": "email"}, + values, + ) + ) + == "mocked" + ) + assert mock_jinja_template.call_args[0][0]["preheader"] == expected_preheader + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args, result, markdown_renderer", + [ + [ + HTMLEmailTemplate, + "email", + {}, + ("the quick brown fox\n" "\n" "jumped over the lazy dog\n"), + "notifications_utils.template.notify_email_markdown", + ], + [ + LetterPreviewTemplate, + "letter", + {}, + ("the quick brown fox\n" "\n" "jumped over the lazy dog\n"), + "notifications_utils.template.notify_letter_preview_markdown", + ], + ], +) +def test_markdown_in_templates( + template_class, + template_type, + extra_args, + result, + markdown_renderer, +): + with mock.patch(markdown_renderer, return_value="") as mock_markdown_renderer: + str( + template_class( + { + "content": ( + "the quick ((colour)) ((animal))\n" + "\n" + "jumped over the lazy dog" + ), + "subject": "animal story", + "template_type": template_type, + }, + {"animal": "fox", "colour": "brown"}, + **extra_args, + ) + ) + mock_markdown_renderer.assert_called_once_with(result) + + +@pytest.mark.parametrize( + "template_class, template_type, extra_attributes", + [ + (HTMLEmailTemplate, "email", 'style="word-wrap: break-word; color: #1D70B8;"'), + ( + EmailPreviewTemplate, + "email", + 'style="word-wrap: break-word; color: #1D70B8;"', + ), + (SMSPreviewTemplate, "sms", 'class="govuk-link govuk-link--no-visited-state"'), + ( + BroadcastPreviewTemplate, + "broadcast", + 'class="govuk-link govuk-link--no-visited-state"', + ), + pytest.param( + SMSBodyPreviewTemplate, + "sms", + 'style="word-wrap: break-word;', + marks=pytest.mark.xfail, + ), + ], +) +@pytest.mark.parametrize( + "url, url_with_entities_replaced", + [ + ("http://example.com", "http://example.com"), + ("http://www.gov.uk/", "http://www.gov.uk/"), + ("https://www.gov.uk/", "https://www.gov.uk/"), + ("http://service.gov.uk", "http://service.gov.uk"), + ( + "http://service.gov.uk/blah.ext?q=a%20b%20c&order=desc#fragment", + "http://service.gov.uk/blah.ext?q=a%20b%20c&order=desc#fragment", + ), + pytest.param("example.com", "example.com", marks=pytest.mark.xfail), + pytest.param("www.example.com", "www.example.com", marks=pytest.mark.xfail), + pytest.param( + "http://service.gov.uk/blah.ext?q=one two three", + "http://service.gov.uk/blah.ext?q=one two three", + marks=pytest.mark.xfail, + ), + pytest.param("ftp://example.com", "ftp://example.com", marks=pytest.mark.xfail), + pytest.param( + "mailto:test@example.com", + "mailto:test@example.com", + marks=pytest.mark.xfail, + ), + ], +) +def test_makes_links_out_of_URLs( + extra_attributes, template_class, template_type, url, url_with_entities_replaced +): + assert '{}'.format( + extra_attributes, url_with_entities_replaced, url_with_entities_replaced + ) in str( + template_class({"content": url, "subject": "", "template_type": template_type}) + ) + + +@pytest.mark.parametrize( + "template_class, template_type", + ( + (SMSPreviewTemplate, "sms"), + (BroadcastPreviewTemplate, "broadcast"), + ), +) +@pytest.mark.parametrize( + "url, url_with_entities_replaced", + ( + ("example.com", "example.com"), + ("www.gov.uk/", "www.gov.uk/"), + ("service.gov.uk", "service.gov.uk"), + ("gov.uk/coronavirus", "gov.uk/coronavirus"), + ( + "service.gov.uk/blah.ext?q=a%20b%20c&order=desc#fragment", + "service.gov.uk/blah.ext?q=a%20b%20c&order=desc#fragment", + ), + ), +) +def test_makes_links_out_of_URLs_without_protocol_in_sms_and_broadcast( + template_class, + template_type, + url, + url_with_entities_replaced, +): + assert ( + f"' + f"{url_with_entities_replaced}" + f"" + ) in str( + template_class({"content": url, "subject": "", "template_type": template_type}) + ) + + +@pytest.mark.parametrize( + "content, html_snippet", + ( + ( + ( + "You've been invited to a service. Click this link:\n" + "https://service.example.com/accept_invite/a1b2c3d4\n" + "\n" + "Thanks\n" + ), + ( + '' + "https://service.example.com/accept_invite/a1b2c3d4" + "" + ), + ), + ( + ("https://service.example.com/accept_invite/?a=b&c=d&"), + ( + '' + "https://service.example.com/accept_invite/?a=b&c=d&" + "" + ), + ), + ), +) +def test_HTML_template_has_URLs_replaced_with_links(content, html_snippet): + assert html_snippet in str( + HTMLEmailTemplate({"content": content, "subject": "", "template_type": "email"}) + ) + + +@pytest.mark.parametrize( + "template_content,expected", + [ + ("gov.uk", "gov.\u200Buk"), + ("GOV.UK", "GOV.\u200BUK"), + ("Gov.uk", "Gov.\u200Buk"), + ("https://gov.uk", "https://gov.uk"), + ("https://www.gov.uk", "https://www.gov.uk"), + ("www.gov.uk", "www.gov.uk"), + ("gov.uk/register-to-vote", "gov.uk/register-to-vote"), + ("gov.uk?q=", "gov.uk?q="), + ], +) +def test_escaping_govuk_in_email_templates(template_content, expected): + assert unlink_govuk_escaped(template_content) == expected + assert expected in str( + PlainTextEmailTemplate( + { + "content": template_content, + "subject": "", + "template_type": "email", + } + ) + ) + assert expected in str( + HTMLEmailTemplate( + { + "content": template_content, + "subject": "", + "template_type": "email", + } + ) + ) + + +def test_stripping_of_unsupported_characters_in_email_templates(): + template_content = "line one\u2028line two" + expected = "line oneline two" + assert expected in str( + PlainTextEmailTemplate( + { + "content": template_content, + "subject": "", + "template_type": "email", + } + ) + ) + assert expected in str( + HTMLEmailTemplate( + { + "content": template_content, + "subject": "", + "template_type": "email", + } + ) + ) + + +@mock.patch("notifications_utils.template.add_prefix", return_value="") +@pytest.mark.parametrize( + "template_class, prefix, body, expected_call", + [ + (SMSMessageTemplate, "a", "b", (Markup("b"), "a")), + (SMSPreviewTemplate, "a", "b", (Markup("b"), "a")), + (BroadcastPreviewTemplate, "a", "b", (Markup("b"), "a")), + (SMSMessageTemplate, None, "b", (Markup("b"), None)), + (SMSPreviewTemplate, None, "b", (Markup("b"), None)), + (BroadcastPreviewTemplate, None, "b", (Markup("b"), None)), + (SMSMessageTemplate, "ht&ml", "b", (Markup("b"), "ht&ml")), + ( + SMSPreviewTemplate, + "ht&ml", + "b", + (Markup("b"), "<em>ht&ml</em>"), + ), + ( + BroadcastPreviewTemplate, + "ht&ml", + "b", + (Markup("b"), "<em>ht&ml</em>"), + ), + ], +) +def test_sms_message_adds_prefix( + add_prefix, template_class, prefix, body, expected_call +): + template = template_class( + {"content": body, "template_type": template_class.template_type} + ) + template.prefix = prefix + template.sender = None + str(template) + add_prefix.assert_called_once_with(*expected_call) + + +@mock.patch("notifications_utils.template.add_prefix", return_value="") +@pytest.mark.parametrize( + "template_class", + [ + SMSMessageTemplate, + SMSPreviewTemplate, + BroadcastPreviewTemplate, + ], +) +@pytest.mark.parametrize( + "show_prefix, prefix, body, sender, expected_call", + [ + (False, "a", "b", "c", (Markup("b"), None)), + (True, "a", "b", None, (Markup("b"), "a")), + (True, "a", "b", False, (Markup("b"), "a")), + ], +) +def test_sms_message_adds_prefix_only_if_asked_to( + add_prefix, + show_prefix, + prefix, + body, + sender, + expected_call, + template_class, +): + template = template_class( + {"content": body, "template_type": template_class.template_type}, + prefix=prefix, + show_prefix=show_prefix, + sender=sender, + ) + str(template) + add_prefix.assert_called_once_with(*expected_call) + + +@pytest.mark.parametrize("content_to_look_for", ["GOVUK", "sms-message-sender"]) +@pytest.mark.parametrize( + "show_sender", + [ + True, + pytest.param(False, marks=pytest.mark.xfail), + ], +) +def test_sms_message_preview_shows_sender( + show_sender, + content_to_look_for, +): + assert content_to_look_for in str( + SMSPreviewTemplate( + {"content": "foo", "template_type": "sms"}, + sender="GOVUK", + show_sender=show_sender, + ) + ) + + +def test_sms_message_preview_hides_sender_by_default(): + assert ( + SMSPreviewTemplate({"content": "foo", "template_type": "sms"}).show_sender + is False + ) + + +@mock.patch("notifications_utils.template.sms_encode", return_value="downgraded") +@pytest.mark.parametrize( + "template_class, extra_args, expected_call", + ( + (SMSMessageTemplate, {"prefix": "Service name"}, "Service name: Message"), + (SMSPreviewTemplate, {"prefix": "Service name"}, "Service name: Message"), + (BroadcastMessageTemplate, {}, "Message"), + (BroadcastPreviewTemplate, {"prefix": "Service name"}, "Service name: Message"), + (SMSBodyPreviewTemplate, {}, "Message"), + ), +) +def test_sms_messages_downgrade_non_sms( + mock_sms_encode, + template_class, + extra_args, + expected_call, +): + template = str( + template_class( + {"content": "Message", "template_type": template_class.template_type}, + **extra_args, + ) + ) + assert "downgraded" in str(template) + mock_sms_encode.assert_called_once_with(expected_call) + + +@pytest.mark.parametrize( + "template_class", + ( + SMSPreviewTemplate, + BroadcastPreviewTemplate, + ), +) +@mock.patch("notifications_utils.template.sms_encode", return_value="downgraded") +def test_sms_messages_dont_downgrade_non_sms_if_setting_is_false( + mock_sms_encode, template_class +): + template = str( + template_class( + {"content": "😎", "template_type": template_class.template_type}, + prefix="👉", + downgrade_non_sms_characters=False, + ) + ) + assert "👉: 😎" in str(template) + assert mock_sms_encode.called is False + + +@pytest.mark.parametrize( + "template_class", + ( + SMSPreviewTemplate, + BroadcastPreviewTemplate, + ), +) +@mock.patch("notifications_utils.template.nl2br") +def test_sms_preview_adds_newlines(nl2br, template_class): + content = "the\nquick\n\nbrown fox" + str( + template_class( + {"content": content, "template_type": template_class.template_type} + ) + ) + nl2br.assert_called_once_with(content) + + +@pytest.mark.parametrize( + "content", + [ + ("one newline\n" "two newlines\n" "\n" "end"), # Unix-style + ("one newline\r\n" "two newlines\r\n" "\r\n" "end"), # Windows-style + ("one newline\r" "two newlines\r" "\r" "end"), # Mac Classic style + ( # A mess + "\t\t\n\r one newline\n" "two newlines\r" "\r\n" "end\n\n \r \n \t " + ), + ], +) +def test_sms_message_normalises_newlines(content): + assert repr( + str(SMSMessageTemplate({"content": content, "template_type": "sms"})) + ) == repr("one newline\n" "two newlines\n" "\n" "end") + + +@pytest.mark.parametrize( + "content", + [ + ("one newline\n" "two newlines\n" "\n" "end"), # Unix-style + ("one newline\r\n" "two newlines\r\n" "\r\n" "end"), # Windows-style + ("one newline\r" "two newlines\r" "\r" "end"), # Mac Classic style + ( # A mess + "\t\t\n\r one newline\xa0\n" "two newlines\r" "\r\n" "end\n\n \r \n \t " + ), + ], +) +def test_broadcast_message_normalises_newlines(content): + assert str( + BroadcastMessageTemplate({"content": content, "template_type": "broadcast"}) + ) == ("one newline\n" "two newlines\n" "\n" "end") + + +@pytest.mark.parametrize( + "template_class", + ( + SMSMessageTemplate, + SMSBodyPreviewTemplate, + BroadcastMessageTemplate, + # Note: SMSPreviewTemplate and BroadcastPreviewTemplate not tested here + # as both will render full HTML template, not just the body + ), +) +def test_phone_templates_normalise_whitespace(template_class): + content = " Hi\u00A0there\u00A0 what's\u200D up\t" + assert ( + str( + template_class( + {"content": content, "template_type": template_class.template_type} + ) + ) + == "Hi there what's up" + ) + + +@freeze_time("2012-12-12 12:12:12") +@mock.patch("notifications_utils.template.LetterPreviewTemplate.jinja_template.render") +@mock.patch("notifications_utils.template.unlink_govuk_escaped") +@mock.patch( + "notifications_utils.template.notify_letter_preview_markdown", return_value="Bar" +) +@pytest.mark.parametrize( + "values, expected_address", + [ + ( + {}, + [ + "address line 1", + "address line 2", + "address line 3", + "address line 4", + "address line 5", + "address line 6", + "address line 7", + ], + ), + ( + { + "address line 1": "123 Fake Street", + "address line 6": "United Kingdom", + }, + [ + "123 Fake Street", + "address line 2", + "address line 3", + "address line 4", + "address line 5", + "United Kingdom", + "address line 7", + ], + ), + ( + { + "address line 1": "123 Fake Street", + "address line 2": "City of Town", + "postcode": "SW1A 1AA", + }, + [ + "123 Fake Street", + "City of Town", + "SW1A 1AA", + ], + ), + ], +) +@pytest.mark.parametrize( + "contact_block, expected_rendered_contact_block", + [ + (None, ""), + ("", ""), + ( + """ + The Pension Service + Mail Handling Site A + Wolverhampton WV9 1LU + + Telephone: 0845 300 0168 + Email: fpc.customercare@dwp.gsi.gov.uk + Monday - Friday 8am - 6pm + www.gov.uk + """, + ( + "The Pension Service
    " + "Mail Handling Site A
    " + "Wolverhampton WV9 1LU
    " + "
    " + "Telephone: 0845 300 0168
    " + "Email: fpc.customercare@dwp.gsi.gov.uk
    " + "Monday - Friday 8am - 6pm
    " + "www.gov.uk" + ), + ), + ], +) +@pytest.mark.parametrize( + "extra_args, expected_logo_file_name, expected_logo_class", + [ + ({}, None, None), + ({"logo_file_name": "example.foo"}, "example.foo", "foo"), + ], +) +@pytest.mark.parametrize( + "additional_extra_args, expected_date", + [ + ({}, "12 December 2012"), + ({"date": None}, "12 December 2012"), + # ({'date': datetime.date.fromtimestamp(0)}, '1 January 1970'), + ], +) +def test_letter_preview_renderer( + letter_markdown, + unlink_govuk, + jinja_template, + values, + expected_address, + contact_block, + expected_rendered_contact_block, + extra_args, + expected_logo_file_name, + expected_logo_class, + additional_extra_args, + expected_date, +): + extra_args.update(additional_extra_args) + str( + LetterPreviewTemplate( + {"content": "Foo", "subject": "Subject", "template_type": "letter"}, + values, + contact_block=contact_block, + **extra_args, + ) + ) + jinja_template.assert_called_once_with( + { + "address": expected_address, + "subject": "Subject", + "message": "Bar", + "date": expected_date, + "contact_block": expected_rendered_contact_block, + "admin_base_url": "http://localhost:6012", + "logo_file_name": expected_logo_file_name, + "logo_class": expected_logo_class, + } + ) + letter_markdown.assert_called_once_with(Markup("Foo\n")) + unlink_govuk.assert_not_called() + + +@freeze_time("2001-01-01 12:00:00.000000") +@mock.patch("notifications_utils.template.LetterPreviewTemplate.jinja_template.render") +def test_letter_preview_renderer_without_mocks(jinja_template): + str( + LetterPreviewTemplate( + {"content": "Foo", "subject": "Subject", "template_type": "letter"}, + {"addressline1": "name", "addressline2": "street", "postcode": "SW1 1AA"}, + contact_block="", + ) + ) + + jinja_template_locals = jinja_template.call_args_list[0][0][0] + + assert jinja_template_locals["address"] == [ + "name", + "street", + "SW1 1AA", + ] + assert jinja_template_locals["subject"] == "Subject" + assert jinja_template_locals["message"] == "

    Foo

    " + assert jinja_template_locals["date"] == "1 January 2001" + assert jinja_template_locals["contact_block"] == "" + assert jinja_template_locals["admin_base_url"] == "http://localhost:6012" + assert jinja_template_locals["logo_file_name"] is None + + +@freeze_time("2012-12-12 12:12:12") +@mock.patch("notifications_utils.template.LetterImageTemplate.jinja_template.render") +@pytest.mark.parametrize( + "page_count, expected_oversized, expected_page_numbers", + [ + ( + 1, + False, + [1], + ), + ( + 5, + False, + [1, 2, 3, 4, 5], + ), + ( + 10, + False, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ), + ( + 11, + True, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ), + ( + 99, + True, + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ), + ], +) +@pytest.mark.parametrize( + "postage_args, expected_show_postage, expected_postage_class_value, expected_postage_description", + ( + pytest.param({}, False, None, None), + pytest.param({"postage": None}, False, None, None), + pytest.param({"postage": "first"}, True, "letter-postage-first", "first class"), + pytest.param( + {"postage": "second"}, True, "letter-postage-second", "second class" + ), + pytest.param( + {"postage": "europe"}, True, "letter-postage-international", "international" + ), + pytest.param( + {"postage": "rest-of-world"}, + True, + "letter-postage-international", + "international", + ), + pytest.param( + {"postage": "third"}, + True, + "letter-postage-third", + "third class", + marks=pytest.mark.xfail(raises=TypeError), + ), + ), +) +def test_letter_image_renderer( + jinja_template, + page_count, + expected_page_numbers, + expected_oversized, + postage_args, + expected_show_postage, + expected_postage_class_value, + expected_postage_description, +): + str( + LetterImageTemplate( + {"content": "Content", "subject": "Subject", "template_type": "letter"}, + image_url="http://example.com/endpoint.png", + page_count=page_count, + contact_block="10 Downing Street", + **postage_args, + ) + ) + jinja_template.assert_called_once_with( + { + "image_url": "http://example.com/endpoint.png", + "page_numbers": expected_page_numbers, + "address": [ + "address line 1", + "address line 2", + "address line 3", + "address line 4", + "address line 5", + "address line 6", + "address line 7", + ], + "contact_block": "10 Downing Street", + "date": "12 December 2012", + "subject": "Subject", + "message": "

    Content

    ", + "show_postage": expected_show_postage, + "postage_class_value": expected_postage_class_value, + "postage_description": expected_postage_description, + } + ) + + +@freeze_time("2012-12-12 12:12:12") +@mock.patch("notifications_utils.template.LetterImageTemplate.jinja_template.render") +@pytest.mark.parametrize( + "postage_argument", + ( + None, + "first", + "second", + "europe", + "rest-of-world", + ), +) +def test_letter_image_renderer_shows_international_post( + jinja_template, + postage_argument, +): + str( + LetterImageTemplate( + {"content": "Content", "subject": "Subject", "template_type": "letter"}, + { + "address line 1": "123 Example Street", + "address line 2": "Lima", + "address line 3": "Peru", + }, + image_url="http://example.com/endpoint.png", + page_count=1, + postage=postage_argument, + ) + ) + assert jinja_template.call_args_list[0][0][0]["postage_description"] == ( + "international" + ) + + +def test_letter_image_template_renders_visually_hidden_address(): + template = BeautifulSoup( + str( + LetterImageTemplate( + {"content": "", "subject": "", "template_type": "letter"}, + { + "address_line_1": "line 1", + "address_line_2": "line 2", + "postcode": "postcode", + }, + image_url="http://example.com/endpoint.png", + page_count=1, + ) + ), + features="html.parser", + ) + assert str(template.select_one(".govuk-visually-hidden ul")) == ( + "
      " "
    • line 1
    • " "
    • line 2
    • " "
    • postcode
    • " "
    " + ) + + +@pytest.mark.parametrize( + "page_image_url", + [ + pytest.param("http://example.com/endpoint.png?page=0", marks=pytest.mark.xfail), + "http://example.com/endpoint.png?page=1", + "http://example.com/endpoint.png?page=2", + "http://example.com/endpoint.png?page=3", + pytest.param("http://example.com/endpoint.png?page=4", marks=pytest.mark.xfail), + ], +) +def test_letter_image_renderer_pagination(page_image_url): + assert page_image_url in str( + LetterImageTemplate( + {"content": "", "subject": "", "template_type": "letter"}, + image_url="http://example.com/endpoint.png", + page_count=3, + ) + ) + + +@pytest.mark.parametrize( + "partial_call, expected_exception, expected_message", + [ + ( + partial(LetterImageTemplate), + TypeError, + "image_url is required", + ), + ( + partial(LetterImageTemplate, page_count=1), + TypeError, + "image_url is required", + ), + ( + partial(LetterImageTemplate, image_url="foo"), + TypeError, + "page_count is required", + ), + ( + partial(LetterImageTemplate, image_url="foo", page_count="foo"), + ValueError, + "invalid literal for int() with base 10: 'foo'", + ), + ( + partial( + LetterImageTemplate, image_url="foo", page_count=1, postage="third" + ), + TypeError, + "postage must be None, 'first', 'second', 'europe' or 'rest-of-world'", + ), + ], +) +def test_letter_image_renderer_requires_arguments( + partial_call, + expected_exception, + expected_message, +): + with pytest.raises(expected_exception) as exception: + partial_call({"content": "", "subject": "", "template_type": "letter"}) + assert str(exception.value) == expected_message + + +@pytest.mark.parametrize( + "postage, expected_attribute_value, expected_postage_text", + ( + (None, None, None), + ( + "first", + ["letter-postage", "letter-postage-first"], + "Postage: first class", + ), + ( + "second", + ["letter-postage", "letter-postage-second"], + "Postage: second class", + ), + ( + "europe", + ["letter-postage", "letter-postage-international"], + "Postage: international", + ), + ( + "rest-of-world", + ["letter-postage", "letter-postage-international"], + "Postage: international", + ), + ), +) +def test_letter_image_renderer_passes_postage_to_html_attribute( + postage, + expected_attribute_value, + expected_postage_text, +): + template = BeautifulSoup( + str( + LetterImageTemplate( + {"content": "", "subject": "", "template_type": "letter"}, + image_url="foo", + page_count=1, + postage=postage, + ) + ), + features="html.parser", + ) + if expected_attribute_value: + assert ( + template.select_one(".letter-postage")["class"] == expected_attribute_value + ) + assert ( + template.select_one(".letter-postage").text.strip() == expected_postage_text + ) + else: + assert not template.select(".letter-postage") + + +@pytest.mark.parametrize( + "template_class", + ( + SMSBodyPreviewTemplate, + SMSMessageTemplate, + SMSPreviewTemplate, + BroadcastMessageTemplate, + BroadcastPreviewTemplate, + ), +) +@pytest.mark.parametrize( + "template_json", + ( + {"content": ""}, + {"content": "", "subject": "subject"}, + ), +) +def test_sms_templates_have_no_subject(template_class, template_json): + template_json.update(template_type=template_class.template_type) + assert not hasattr( + template_class(template_json), + "subject", + ) + + +def test_subject_line_gets_applied_to_correct_template_types(): + for cls in [ + EmailPreviewTemplate, + HTMLEmailTemplate, + PlainTextEmailTemplate, + LetterPreviewTemplate, + LetterImageTemplate, + ]: + assert issubclass(cls, SubjectMixin) + for cls in [ + SMSBodyPreviewTemplate, + SMSMessageTemplate, + SMSPreviewTemplate, + BroadcastMessageTemplate, + BroadcastPreviewTemplate, + ]: + assert not issubclass(cls, SubjectMixin) + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args", + ( + (EmailPreviewTemplate, "email", {}), + (HTMLEmailTemplate, "email", {}), + (PlainTextEmailTemplate, "email", {}), + (LetterPreviewTemplate, "letter", {}), + (LetterPrintTemplate, "letter", {}), + ( + LetterImageTemplate, + "letter", + { + "image_url": "http://example.com", + "page_count": 1, + }, + ), + ), +) +def test_subject_line_gets_replaced(template_class, template_type, extra_args): + template = template_class( + {"content": "", "template_type": template_type, "subject": "((name))"}, + **extra_args, + ) + assert template.subject == Markup("((name))") + template.values = {"name": "Jo"} + assert template.subject == "Jo" + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args", + ( + (EmailPreviewTemplate, "email", {}), + (HTMLEmailTemplate, "email", {}), + (PlainTextEmailTemplate, "email", {}), + (LetterPreviewTemplate, "letter", {}), + (LetterPrintTemplate, "letter", {}), + ( + LetterImageTemplate, + "letter", + { + "image_url": "http://example.com", + "page_count": 1, + }, + ), + ), +) +@pytest.mark.parametrize( + "content, values, expected_count", + [ + ("Content with ((placeholder))", {"placeholder": "something extra"}, 28), + ("Content with ((placeholder))", {"placeholder": ""}, 12), + ("Just content", {}, 12), + ("((placeholder)) ", {"placeholder": " "}, 0), + (" ", {}, 0), + ], +) +def test_character_count_for_non_sms_templates( + template_class, + template_type, + extra_args, + content, + values, + expected_count, +): + template = template_class( + { + "content": content, + "subject": "Hi", + "template_type": template_type, + }, + **extra_args, + ) + template.values = values + assert template.content_count == expected_count + + +@pytest.mark.parametrize( + "template_class", + [ + SMSMessageTemplate, + SMSPreviewTemplate, + ], +) +@pytest.mark.parametrize( + "content, values, prefix, expected_count_in_template, expected_count_in_notification", + [ + # is an unsupported unicode character so should be replaced with a ? + ("æˇą", {}, None, 1, 1), + # is a supported unicode character so should be kept as is + ("Å´", {}, None, 1, 1), + ("'First line.\n", {}, None, 12, 12), + ("\t\n\r", {}, None, 0, 0), + ( + "Content with ((placeholder))", + {"placeholder": "something extra here"}, + None, + 13, + 33, + ), + ("Content with ((placeholder))", {"placeholder": ""}, None, 13, 12), + ("Just content", {}, None, 12, 12), + ("((placeholder)) ", {"placeholder": " "}, None, 0, 0), + (" ", {}, None, 0, 0), + ( + "Content with ((placeholder))", + {"placeholder": "something extra here"}, + "GDS", + 18, + 38, + ), + ("Just content", {}, "GDS", 17, 17), + ("((placeholder)) ", {"placeholder": " "}, "GDS", 5, 4), + (" ", {}, "GDS", 4, 4), # Becomes `GDS:` + (" G D S ", {}, None, 5, 5), # Becomes `G D S` + ("P1 \n\n\n\n\n\n P2", {}, None, 6, 6), # Becomes `P1\n\nP2` + ( + "a ((placeholder)) b", + {"placeholder": ""}, + None, + 4, + 3, + ), # Counted as `a b` then `a b` + ], +) +def test_character_count_for_sms_templates( + content, + values, + prefix, + expected_count_in_template, + expected_count_in_notification, + template_class, +): + template = template_class( + {"content": content, "template_type": "sms"}, + prefix=prefix, + ) + template.sender = None + assert template.content_count == expected_count_in_template + template.values = values + assert template.content_count == expected_count_in_notification + + +@pytest.mark.parametrize( + "template_class", + [ + BroadcastMessageTemplate, + BroadcastPreviewTemplate, + ], +) +@pytest.mark.parametrize( + "content, values, expected_count_in_template, expected_count_in_notification", + [ + # is an unsupported unicode character so should be replaced with a ? + ("æˇą", {}, 1, 1), + # is a supported unicode character so should be kept as is + ("Å´", {}, 1, 1), + ("'First line.\n", {}, 12, 12), + ("\t\n\r", {}, 0, 0), + ( + "Content with ((placeholder))", + {"placeholder": "something extra here"}, + 13, + 33, + ), + ("Content with ((placeholder))", {"placeholder": ""}, 13, 12), + ("Just content", {}, 12, 12), + ("((placeholder)) ", {"placeholder": " "}, 0, 0), + (" ", {}, 0, 0), + (" G D S ", {}, 5, 5), # Becomes `G D S` + ("P1 \n\n\n\n\n\n P2", {}, 6, 6), # Becomes `P1\n\nP2` + ], +) +def test_character_count_for_broadcast_templates( + content, + values, + expected_count_in_template, + expected_count_in_notification, + template_class, +): + template = template_class( + {"content": content, "template_type": "broadcast"}, + ) + assert template.content_count == expected_count_in_template + template.values = values + assert template.content_count == expected_count_in_notification + + +@pytest.mark.parametrize( + "template_class", + ( + SMSMessageTemplate, + BroadcastMessageTemplate, + ), +) +@pytest.mark.parametrize( + "msg, expected_sms_fragment_count", + [ + ( + """This is a very long long long long long long long long long long + long long long long long long long long long long long long long long text message.""", + 1, + ), + ("This is a short message.", 1), + ], +) +def test_sms_fragment_count_accounts_for_unicode_and_welsh_characters( + template_class, + msg, + expected_sms_fragment_count, +): + template = template_class( + {"content": msg, "template_type": template_class.template_type} + ) + assert template.fragment_count == expected_sms_fragment_count + + +@pytest.mark.parametrize( + "template_class", + ( + SMSMessageTemplate, + BroadcastMessageTemplate, + ), +) +@pytest.mark.parametrize( + "msg, expected_sms_fragment_count", + [ + # all extended GSM characters + ( + "Đ­Ņ‚Đž Đ´ĐģиĐŊĐŊĐžĐĩ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ ĐŊа Ņ€ŅƒŅŅĐēĐžĐŧ ŅĐˇŅ‹ĐēĐĩ, Ņ‡Ņ‚ĐžĐąŅ‹ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đ¸Ņ‚ŅŒ, ĐēаĐē ŅĐ¸ŅŅ‚ĐĩĐŧа Ņ€Đ°ŅŅŅ‡Đ¸Ņ‚Ņ‹Đ˛Đ°ĐĩŅ‚ ĐĩĐŗĐž ŅŅ‚ĐžĐ¸ĐŧĐžŅŅ‚ŅŒ.", + 2, + ), + ( + "ė´ę˛ƒė€ ë§¤ėš° 描溠 ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ ė˜¤ëž˜ 긴 ëŦ¸ėž ëŠ”ė‹œė§€ėž…ë‹ˆë‹¤.", + 2, + ), + ("Î‘Ī…Ī„ĪŒ ÎĩίÎŊιΚ έÎŊÎą ÎŧÎĩÎŗÎŦÎģÎŋ ÎŧÎŽÎŊĪ…ÎŧÎą ĪƒĪ„Îą ĪĪ‰ĪƒÎšÎēÎŦ ÎŗÎšÎą ÎŊÎą ÎĩÎģÎ­ÎŗÎžÎĩĪ„Îĩ Ī€ĪŽĪ‚ Ī„Îŋ ÎŗÎšÎą ÎąĪ…Ī„ĪŒ", 2), + ( + "ã“ã‚Œã¯ã€ã‚ˇã‚šãƒ†ãƒ ãŒã‚ŗã‚šãƒˆã‚’ãŠãŽã‚ˆã†ãĢč¨ˆįŽ—ã™ã‚‹ã‹ã‚’ãƒ†ã‚šãƒˆã™ã‚‹ãŸã‚ãŽãƒ­ã‚ˇã‚ĸčĒžãŽé•ˇã„ãƒĄãƒƒã‚ģãƒŧジです", + 1, + ), + ("čŋ™æ˜¯ä¸€æĄåžˆé•ŋįš„äŋ„蝭æļˆæ¯īŧŒį”¨äēŽæĩ‹č¯•įŗģįģŸåĻ‚äŊ•čŽĄįŽ—å…ļ成æœŦ", 1), + ( + "čŋ™æ˜¯ä¸€ä¸Ē非常é•ŋįš„é•ŋé•ŋé•ŋé•ŋįš„é•ŋé•ŋé•ŋé•ŋįš„é•ŋé•ŋé•ŋé•ŋįš„é•ŋé•ŋé•ŋé•ŋįš„é•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋé•ŋįš„é•ŋé•ŋé•ŋé•ŋįš„é•ŋį¯‡įŸ­äŋĄ", + 1, + ), + ( + "ã“ã‚Œã¯ã€ã‚ˇã‚šãƒ†ãƒ ãŒã‚ŗã‚šãƒˆã‚’ãŠãŽã‚ˆã†ãĢč¨ˆįŽ—ã™ã‚‹ã‹ã‚’ãƒ†ã‚šãƒˆã™ã‚‹ãŸã‚ãŽãƒ­ã‚ˇã‚ĸčĒžãŽé•ˇã„ãƒĄãƒƒã‚ģãƒŧジです foo foofoofoofoofoofoofoofoo", + 2, + ), + ( + "Đ­Ņ‚Đž Đ´ĐģиĐŊĐŊĐžĐĩ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ ĐŊа Ņ€ŅƒŅŅĐēĐžĐŧ ŅĐˇŅ‹ĐēĐĩ, Ņ‡Ņ‚ĐžĐąŅ‹ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đ¸Ņ‚ŅŒ, ĐēаĐē ŅĐ¸ŅŅ‚ĐĩĐŧа Ņ€Đ°ŅŅŅ‡Đ¸Ņ‚Ņ‹Đ˛Đ°ĐĩŅ‚ ĐĩĐŗĐž ŅŅ‚ĐžĐ¸ĐŧĐžŅŅ‚ŅŒ.\ + foo foo foo foo foo foo foo foo foo foo", + 3, + ), + ( + "Hello Carlos. Your Example Corp. bill of $100 is now available. Autopay is scheduled for next Thursday,\ + April 9. To view the details of your bill, go to https://example.com/bill1.", + 2, + ), + ( + "äēšéŠŦ逊å…Ŧ司是一åŽļæ€ģ部äŊäēŽįžŽå›ŊčĨŋé›…å›žįš„čˇ¨å›Ŋį”ĩå­å•†åŠĄäŧä¸šīŧŒä¸šåŠĄčĩˇå§‹äēŽįēŋ上äšĻåē—īŧŒä¸äš…䚋后商品čĩ°å‘多元化。杰å¤Ģ·贝äŊæ–¯äēŽ1994åš´7月创åģēäē†čŋ™åŽļå…Ŧ司。", + 2, + ), + # This test should break into two messages, but \u2019 gets converted to (') + ( + "John: Your appointment with Dr. Salazar’s office is scheduled for next Thursday at 4:30pm.\ + Reply YES to confirm, NO to reschedule.", + 1, + ), + ], +) +def test_sms_fragment_count_accounts_for_non_latin_characters( + template_class, + msg, + expected_sms_fragment_count, +): + template = template_class( + {"content": msg, "template_type": template_class.template_type} + ) + assert template.fragment_count == expected_sms_fragment_count + + +@pytest.mark.parametrize( + "template_class", + [ + SMSMessageTemplate, + SMSPreviewTemplate, + ], +) +@pytest.mark.parametrize( + "content, values, prefix, expected_result", + [ + ("", {}, None, True), + ("", {}, "GDS", True), + ("((placeholder))", {"placeholder": ""}, "GDS", True), + ("((placeholder))", {"placeholder": "Some content"}, None, False), + ("Some content", {}, "GDS", False), + ], +) +def test_is_message_empty_sms_templates( + content, values, prefix, expected_result, template_class +): + template = template_class( + {"content": content, "template_type": "sms"}, + prefix=prefix, + ) + template.sender = None + template.values = values + assert template.is_message_empty() == expected_result + + +@pytest.mark.parametrize( + "template_class", + [ + BroadcastMessageTemplate, + BroadcastPreviewTemplate, + ], +) +@pytest.mark.parametrize( + "content, values, expected_result", + [ + ("", {}, True), + ("((placeholder))", {"placeholder": ""}, True), + ("((placeholder))", {"placeholder": "Some content"}, False), + ("Some content", {}, False), + ], +) +def test_is_message_empty_broadcast_templates( + content, values, expected_result, template_class +): + template = template_class( + {"content": content, "template_type": "broadcast"}, + ) + template.sender = None + template.values = values + assert template.is_message_empty() == expected_result + + +@pytest.mark.parametrize( + "template_class, template_type", + ( + (HTMLEmailTemplate, "email"), + (LetterPrintTemplate, "letter"), + ), +) +@pytest.mark.parametrize( + "content, values, expected_result", + [ + ("", {}, True), + ("((placeholder))", {"placeholder": ""}, True), + ("((placeholder))", {"placeholder": " \t \r\n"}, True), + ("((placeholder))", {"placeholder": "Some content"}, False), + ("((placeholder??show_or_hide))", {"placeholder": False}, True), + ("Some content", {}, False), + ("((placeholder)) some content", {"placeholder": ""}, False), + ("Some content ((placeholder))", {"placeholder": ""}, False), + ], +) +def test_is_message_empty_email_and_letter_templates( + template_class, + template_type, + content, + values, + expected_result, +): + template = template_class( + { + "content": content, + "subject": "Hi", + "template_type": template_class.template_type, + } + ) + template.sender = None + template.values = values + assert template.is_message_empty() == expected_result + + +@pytest.mark.parametrize( + "template_class, template_type", + ( + (HTMLEmailTemplate, "email"), + (LetterPrintTemplate, "letter"), + ), +) +@pytest.mark.parametrize( + "content, values", + [ + ("Some content", {}), + ("((placeholder)) some content", {"placeholder": ""}), + ("Some content ((placeholder))", {"placeholder": ""}), + pytest.param( + "((placeholder))", + {"placeholder": "Some content"}, + marks=pytest.mark.xfail(raises=AssertionError), + ), + ], +) +def test_is_message_empty_email_and_letter_templates_tries_not_to_count_chars( + mocker, + template_class, + template_type, + content, + values, +): + template = template_class( + { + "content": content, + "subject": "Hi", + "template_type": template_type, + } + ) + mock_content = mocker.patch.object( + template_class, + "content_count", + create=True, + new_callable=mock.PropertyMock, + return_value=None, + ) + template.values = values + template.is_message_empty() + assert mock_content.called is False + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args, expected_field_calls", + [ + ( + PlainTextEmailTemplate, + "email", + {}, + [mock.call("content", {}, html="passthrough", markdown_lists=True)], + ), + ( + HTMLEmailTemplate, + "email", + {}, + [ + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=False + ), + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=False, + ), + mock.call("content", {}, html="escape", markdown_lists=True), + ], + ), + ( + EmailPreviewTemplate, + "email", + {}, + [ + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=False, + ), + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=False + ), + mock.call("((email address))", {}, with_brackets=False), + ], + ), + ( + SMSMessageTemplate, + "sms", + {}, + [ + mock.call("content"), # This is to get the placeholders + mock.call("content", {}, html="passthrough"), + ], + ), + ( + SMSPreviewTemplate, + "sms", + {}, + [ + mock.call("((phone number))", {}, with_brackets=False, html="escape"), + mock.call( + "content", {}, html="escape", redact_missing_personalisation=False + ), + ], + ), + ( + BroadcastMessageTemplate, + "broadcast", + {}, + [ + mock.call("content", {}, html="escape"), + ], + ), + ( + BroadcastPreviewTemplate, + "broadcast", + {}, + [ + mock.call("((phone number))", {}, with_brackets=False, html="escape"), + mock.call( + "content", {}, html="escape", redact_missing_personalisation=False + ), + ], + ), + ( + LetterPreviewTemplate, + "letter", + {"contact_block": "www.gov.uk"}, + [ + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=False + ), + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=False, + ), + mock.call( + ( + "((address line 1))\n" + "((address line 2))\n" + "((address line 3))\n" + "((address line 4))\n" + "((address line 5))\n" + "((address line 6))\n" + "((address line 7))" + ), + {}, + with_brackets=False, + html="escape", + ), + mock.call( + "www.gov.uk", + {}, + html="escape", + redact_missing_personalisation=False, + ), + ], + ), + ( + LetterImageTemplate, + "letter", + { + "image_url": "http://example.com", + "page_count": 1, + "contact_block": "www.gov.uk", + }, + [ + mock.call( + ( + "((address line 1))\n" + "((address line 2))\n" + "((address line 3))\n" + "((address line 4))\n" + "((address line 5))\n" + "((address line 6))\n" + "((address line 7))" + ), + {}, + with_brackets=False, + html="escape", + ), + mock.call( + "www.gov.uk", + {}, + html="escape", + redact_missing_personalisation=False, + ), + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=False + ), + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=False, + ), + ], + ), + ( + EmailPreviewTemplate, + "email", + {"redact_missing_personalisation": True}, + [ + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=True, + ), + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=True + ), + mock.call("((email address))", {}, with_brackets=False), + ], + ), + ( + SMSPreviewTemplate, + "sms", + {"redact_missing_personalisation": True}, + [ + mock.call("((phone number))", {}, with_brackets=False, html="escape"), + mock.call( + "content", {}, html="escape", redact_missing_personalisation=True + ), + ], + ), + ( + BroadcastPreviewTemplate, + "broadcast", + {"redact_missing_personalisation": True}, + [ + mock.call("((phone number))", {}, with_brackets=False, html="escape"), + mock.call( + "content", {}, html="escape", redact_missing_personalisation=True + ), + ], + ), + ( + SMSBodyPreviewTemplate, + "sms", + {}, + [ + mock.call( + "content", {}, html="escape", redact_missing_personalisation=True + ), + ], + ), + ( + LetterPreviewTemplate, + "letter", + {"contact_block": "www.gov.uk", "redact_missing_personalisation": True}, + [ + mock.call( + "subject", {}, html="escape", redact_missing_personalisation=True + ), + mock.call( + "content", + {}, + html="escape", + markdown_lists=True, + redact_missing_personalisation=True, + ), + mock.call( + ( + "((address line 1))\n" + "((address line 2))\n" + "((address line 3))\n" + "((address line 4))\n" + "((address line 5))\n" + "((address line 6))\n" + "((address line 7))" + ), + {}, + with_brackets=False, + html="escape", + ), + mock.call( + "www.gov.uk", {}, html="escape", redact_missing_personalisation=True + ), + ], + ), + ], +) +@mock.patch("notifications_utils.template.Field.__init__", return_value=None) +@mock.patch( + "notifications_utils.template.Field.__str__", return_value="1\n2\n3\n4\n5\n6\n7\n8" +) +def test_templates_handle_html_and_redacting( + mock_field_str, + mock_field_init, + template_class, + template_type, + extra_args, + expected_field_calls, +): + assert str( + template_class( + { + "content": "content", + "subject": "subject", + "template_type": template_type, + }, + **extra_args, + ) + ) + assert mock_field_init.call_args_list == expected_field_calls + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args, expected_remove_whitespace_calls", + [ + ( + PlainTextEmailTemplate, + "email", + {}, + [ + mock.call("\n\ncontent"), + mock.call(Markup("subject")), + mock.call(Markup("subject")), + ], + ), + ( + HTMLEmailTemplate, + "email", + {}, + [ + mock.call(Markup("subject")), + mock.call( + '

    ' + "content" + "

    " + ), + mock.call("\n\ncontent"), + mock.call(Markup("subject")), + mock.call(Markup("subject")), + ], + ), + ( + EmailPreviewTemplate, + "email", + {}, + [ + mock.call( + '

    ' + "content" + "

    " + ), + mock.call(Markup("subject")), + mock.call(Markup("subject")), + mock.call(Markup("subject")), + ], + ), + ( + SMSMessageTemplate, + "sms", + {}, + [ + mock.call("content"), + ], + ), + ( + SMSPreviewTemplate, + "sms", + {}, + [ + mock.call("content"), + ], + ), + ( + SMSBodyPreviewTemplate, + "sms", + {}, + [ + mock.call("content"), + ], + ), + ( + BroadcastMessageTemplate, + "broadcast", + {}, + [ + mock.call("content"), + ], + ), + ( + BroadcastPreviewTemplate, + "broadcast", + {}, + [ + mock.call("content"), + ], + ), + ( + LetterPreviewTemplate, + "letter", + {"contact_block": "www.gov.uk"}, + [ + mock.call(Markup("subject")), + mock.call(Markup("

    content

    ")), + mock.call(Markup("www.gov.uk")), + mock.call(Markup("subject")), + mock.call(Markup("subject")), + ], + ), + ], +) +@mock.patch( + "notifications_utils.template.remove_whitespace_before_punctuation", + side_effect=lambda x: x, +) +def test_templates_remove_whitespace_before_punctuation( + mock_remove_whitespace, + template_class, + template_type, + extra_args, + expected_remove_whitespace_calls, +): + template = template_class( + {"content": "content", "subject": "subject", "template_type": template_type}, + **extra_args, + ) + + assert str(template) + + if hasattr(template, "subject"): + assert template.subject + + assert mock_remove_whitespace.call_args_list == expected_remove_whitespace_calls + + +@pytest.mark.parametrize( + "template_class, template_type, extra_args, expected_calls", + [ + ( + PlainTextEmailTemplate, + "email", + {}, + [ + mock.call("\n\ncontent"), + mock.call(Markup("subject")), + ], + ), + ( + HTMLEmailTemplate, + "email", + {}, + [ + mock.call( + '

    ' + "content" + "

    " + ), + mock.call("\n\ncontent"), + mock.call(Markup("subject")), + ], + ), + ( + EmailPreviewTemplate, + "email", + {}, + [ + mock.call( + '

    ' + "content" + "

    " + ), + mock.call(Markup("subject")), + ], + ), + (SMSMessageTemplate, "sms", {}, []), + (SMSPreviewTemplate, "sms", {}, []), + (SMSBodyPreviewTemplate, "sms", {}, []), + (BroadcastMessageTemplate, "broadcast", {}, []), + (BroadcastPreviewTemplate, "broadcast", {}, []), + ( + LetterPreviewTemplate, + "letter", + {"contact_block": "www.gov.uk"}, + [ + mock.call(Markup("subject")), + mock.call(Markup("

    content

    ")), + ], + ), + ], +) +@mock.patch("notifications_utils.template.make_quotes_smart", side_effect=lambda x: x) +@mock.patch( + "notifications_utils.template.replace_hyphens_with_en_dashes", + side_effect=lambda x: x, +) +def test_templates_make_quotes_smart_and_dashes_en( + mock_en_dash_replacement, + mock_smart_quotes, + template_class, + template_type, + extra_args, + expected_calls, +): + template = template_class( + {"content": "content", "subject": "subject", "template_type": template_type}, + **extra_args, + ) + + assert str(template) + + if hasattr(template, "subject"): + assert template.subject + + mock_smart_quotes.assert_has_calls(expected_calls) + mock_en_dash_replacement.assert_has_calls(expected_calls) + + +@pytest.mark.parametrize( + "content", + ( + "first.o'last@example.com", + "first.o’last@example.com", + ), +) +@pytest.mark.parametrize( + "template_class", + ( + HTMLEmailTemplate, + PlainTextEmailTemplate, + EmailPreviewTemplate, + ), +) +def test_no_smart_quotes_in_email_addresses(template_class, content): + template = template_class( + { + "content": content, + "subject": content, + "template_type": "email", + } + ) + assert "first.o'last@example.com" in str(template) + assert template.subject == "first.o'last@example.com" + + +def test_smart_quotes_removed_from_long_template_in_under_a_second(): + long_string = "a" * 100000 + template = PlainTextEmailTemplate( + { + "content": long_string, + "subject": "", + "template_type": "email", + } + ) + + start_time = process_time() + + str(template) + + assert process_time() - start_time < 1 + + +@pytest.mark.parametrize( + "template_instance, expected_placeholders", + [ + ( + SMSMessageTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "sms", + }, + ), + ["content"], + ), + ( + SMSPreviewTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "sms", + }, + ), + ["content"], + ), + ( + SMSBodyPreviewTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "sms", + }, + ), + ["content"], + ), + ( + BroadcastMessageTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "broadcast", + }, + ), + ["content"], + ), + ( + BroadcastPreviewTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "broadcast", + }, + ), + ["content"], + ), + ( + PlainTextEmailTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "email", + }, + ), + ["subject", "content"], + ), + ( + HTMLEmailTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "email", + }, + ), + ["subject", "content"], + ), + ( + EmailPreviewTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "email", + }, + ), + ["subject", "content"], + ), + ( + LetterPreviewTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "letter", + }, + contact_block="((contact_block))", + ), + ["contact_block", "subject", "content"], + ), + ( + LetterImageTemplate( + { + "content": "((content))", + "subject": "((subject))", + "template_type": "letter", + }, + contact_block="((contact_block))", + image_url="http://example.com", + page_count=99, + ), + ["contact_block", "subject", "content"], + ), + ], +) +def test_templates_extract_placeholders( + template_instance, + expected_placeholders, +): + assert template_instance.placeholders == OrderedSet(expected_placeholders) + + +@pytest.mark.parametrize( + "extra_args", + [ + {"from_name": "Example service"}, + { + "from_name": "Example service", + "from_address": "test@example.com", + }, + pytest.param({}, marks=pytest.mark.xfail), + ], +) +def test_email_preview_shows_from_name(extra_args): + template = EmailPreviewTemplate( + {"content": "content", "subject": "subject", "template_type": "email"}, + **extra_args, + ) + assert 'From' in str(template) + assert "Example service" in str(template) + assert "test@example.com" not in str(template) + + +def test_email_preview_escapes_html_in_from_name(): + template = EmailPreviewTemplate( + {"content": "content", "subject": "subject", "template_type": "email"}, + from_name='', + from_address="test@example.com", + ) + assert "