From 465013d20d260a14630e6f5cec88f6d4512ea22f Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Tue, 30 Mar 2021 13:17:40 -0700 Subject: [PATCH 1/9] Add support for arbitrary json in conn uri format --- airflow/models/connection.py | 18 ++++++- docs/apache-airflow/howto/connection.rst | 68 ++++++++++++++++++------ tests/models/test_connection.py | 48 +++++++++++++++-- 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/airflow/models/connection.py b/airflow/models/connection.py index 67a3b4dd4802a..22675c3ae10c1 100644 --- a/airflow/models/connection.py +++ b/airflow/models/connection.py @@ -18,6 +18,7 @@ import json import warnings +from base64 import urlsafe_b64decode, urlsafe_b64encode from json import JSONDecodeError from typing import Dict, Optional from urllib.parse import parse_qsl, quote, unquote, urlencode, urlparse @@ -89,6 +90,8 @@ class Connection(Base, LoggingMixin): # pylint: disable=too-many-instance-attri :type uri: str """ + EXTRA_BASE64_KEY = '__extra__' + __tablename__ = "connection" id = Column(Integer(), primary_key=True) @@ -161,7 +164,11 @@ def _parse_from_uri(self, uri: str): self.password = unquote(uri_parts.password) if uri_parts.password else uri_parts.password self.port = uri_parts.port if uri_parts.query: - self.extra = json.dumps(dict(parse_qsl(uri_parts.query, keep_blank_values=True))) + query = dict(parse_qsl(uri_parts.query, keep_blank_values=True)) + if self.EXTRA_BASE64_KEY in query: + self.extra = urlsafe_b64decode(query[self.EXTRA_BASE64_KEY]).decode('utf-8') + else: + self.extra = json.dumps(query) def get_uri(self) -> str: """Return connection in URI format""" @@ -195,7 +202,14 @@ def get_uri(self) -> str: uri += host_block if self.extra_dejson: - uri += f'?{urlencode(self.extra_dejson)}' + try: + query = urlencode(self.extra_dejson) + except TypeError: + query = None + if query and self.extra_dejson == dict(parse_qsl(query, keep_blank_values=True)): + uri += '?' + query + else: + uri += '?' + urlencode({self.EXTRA_BASE64_KEY: urlsafe_b64encode(self.extra.encode('utf-8'))}) return uri diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index 2d15324278531..a23c581f35fbc 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -212,10 +212,6 @@ In general, Airflow's URI format is like so: my-conn-type://my-login:my-password@my-host:5432/my-schema?param1=val1¶m2=val2 -.. note:: - - The params ``param1`` and ``param2`` are just examples; you may supply arbitrary urlencoded json-serializable data there. - The above URI would produce a ``Connection`` object equivalent to the following: .. code-block:: python @@ -232,17 +228,6 @@ The above URI would produce a ``Connection`` object equivalent to the following: extra=json.dumps(dict(param1='val1', param2='val2')) ) -You can verify a URI is parsed correctly like so: - -.. code-block:: pycon - - >>> from airflow.models.connection import Connection - - >>> c = Connection(uri='my-conn-type://my-login:my-password@my-host:5432/my-schema?param1=val1¶m2=val2') - >>> print(c.login) - my-login - >>> print(c.password) - my-password .. _generating_connection_uri: @@ -289,12 +274,63 @@ Additionally, if you have created a connection, you can use ``airflow connection .. _manage-connections-connection-types: +Encoding arbitrary json +^^^^^^^^^^^^^^^^^^^^^^^ + +Some json structures cannot be urlencoded without loss. For such jsons, ``get_uri`` +will encode the json as base64 and store under the url query param ``__extra__``. + +For example: + +.. code-block:: pycon + + >>> extra_dict = {'my_val': ['list', 'of', 'values'], 'extra': {'nested': {'json': 'val'}}} + >>> c = Connection( + >>> conn_type='scheme', + >>> host='host/location', + >>> schema='schema', + >>> login='user', + >>> password='password', + >>> port=1234, + >>> extra=json.dumps(extra_dict), + >>> ) + >>> uri = c.get_uri() + >>> uri + 'scheme://user:password@host%2Flocation:1234/schema?__extra__=eyJteV92YWwiOiBbImxpc3QiLCAib2YiLCAidmFsdWVzIl0sICJleHRyYSI6IHsibmVzdGVkIjogeyJqc29uIjogInZhbCJ9fX0%3D' + + +And we can verify that it returns the same dictionary: + +.. code-block:: pycon + + >>> new_c = Connection(uri=uri) + >>> new_c.extra_dejson == extra_dict + True + + +But for the most common case of storing only key-value pairs, plain urlencoding is used. + +You can verify a URI is parsed correctly like so: + +.. code-block:: pycon + + >>> from airflow.models.connection import Connection + + >>> c = Connection(uri='my-conn-type://my-login:my-password@my-host:5432/my-schema?param1=val1¶m2=val2') + >>> print(c.login) + my-login + >>> print(c.password) + my-password + + Handling of special characters in connection params ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. note:: - This process is automated as described in section :ref:`Generating a Connection URI `. + Use the convenience method ``Connection.get_uri`` when generating a connection + as described in section :ref:`Generating a Connection URI `. + This section for informational purposes only. Special handling is required for certain characters when building a URI manually. diff --git a/tests/models/test_connection.py b/tests/models/test_connection.py index 526d029694809..0db43d36cf747 100644 --- a/tests/models/test_connection.py +++ b/tests/models/test_connection.py @@ -137,6 +137,48 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): ), description='with extras', ), + UriTestCaseConfig( + test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' + '__extra__=InNpbmdsZSB2YWx1ZSI%3D', + test_conn_attributes=dict( + conn_type='scheme', + host='host/location', + schema='schema', + login='user', + password='password', + port=1234, + extra_dejson='single value', + ), + description='with extras single value', + ), + UriTestCaseConfig( + test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' + '__extra__=WyJsaXN0IiwgIm9mIiwgInZhbHVlcyJd', + test_conn_attributes=dict( + conn_type='scheme', + host='host/location', + schema='schema', + login='user', + password='password', + port=1234, + extra_dejson=['list', 'of', 'values'], + ), + description='with extras list', + ), + UriTestCaseConfig( + test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' + '__extra__=eyJteV92YWwiOiBbImxpc3QiLCAib2YiLCAidmFsdWVzIl0sICJleHRyYSI6IHsibmVzdGVkIjogeyJqc29uIjogInZhbCJ9fX0%3D', # noqa: E501 # pylint: disable=C0301 + test_conn_attributes=dict( + conn_type='scheme', + host='host/location', + schema='schema', + login='user', + password='password', + port=1234, + extra_dejson={'my_val': ['list', 'of', 'values'], 'extra': {'nested': {'json': 'val'}}}, + ), + description='with nested json', + ), UriTestCaseConfig( test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?extra1=a%20value&extra2=', test_conn_attributes=dict( @@ -351,11 +393,9 @@ def test_connection_get_uri_from_conn(self, test_config: UriTestCaseConfig): for conn_attr, expected_val in test_config.test_conn_attributes.items(): actual_val = getattr(new_conn, conn_attr) if expected_val is None: - assert expected_val is None - if isinstance(expected_val, dict): - assert expected_val == actual_val + assert actual_val is None else: - assert expected_val == actual_val + assert actual_val == expected_val @parameterized.expand( [ From 705248f18f6b94ed05b50cabd5a3c4cd776e69e3 Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Tue, 30 Mar 2021 14:52:39 -0700 Subject: [PATCH 2/9] add support for arbitrary string --- airflow/models/connection.py | 2 +- tests/models/test_connection.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/airflow/models/connection.py b/airflow/models/connection.py index 22675c3ae10c1..149fb517ca9e1 100644 --- a/airflow/models/connection.py +++ b/airflow/models/connection.py @@ -201,7 +201,7 @@ def get_uri(self) -> str: uri += host_block - if self.extra_dejson: + if self.extra: try: query = urlencode(self.extra_dejson) except TypeError: diff --git a/tests/models/test_connection.py b/tests/models/test_connection.py index 0db43d36cf747..058225ff0a076 100644 --- a/tests/models/test_connection.py +++ b/tests/models/test_connection.py @@ -151,6 +151,20 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): ), description='with extras single value', ), + UriTestCaseConfig( + test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' + '__extra__=YXJiaXRyYXJ5IHN0cmluZyAqKSok', + test_conn_attributes=dict( + conn_type='scheme', + host='host/location', + schema='schema', + login='user', + password='password', + port=1234, + extra='arbitrary string *)*$', + ), + description='with extra non-json', + ), UriTestCaseConfig( test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' '__extra__=WyJsaXN0IiwgIm9mIiwgInZhbHVlcyJd', From 42ea70a0ba8801dcac3c563b6dd5f59032b555b1 Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Tue, 30 Mar 2021 16:54:10 -0700 Subject: [PATCH 3/9] dont use base64 --- airflow/models/connection.py | 9 ++++----- docs/apache-airflow/howto/connection.rst | 4 ++-- tests/models/test_connection.py | 11 +++++------ 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/airflow/models/connection.py b/airflow/models/connection.py index 149fb517ca9e1..78c76292b7d3e 100644 --- a/airflow/models/connection.py +++ b/airflow/models/connection.py @@ -18,7 +18,6 @@ import json import warnings -from base64 import urlsafe_b64decode, urlsafe_b64encode from json import JSONDecodeError from typing import Dict, Optional from urllib.parse import parse_qsl, quote, unquote, urlencode, urlparse @@ -90,7 +89,7 @@ class Connection(Base, LoggingMixin): # pylint: disable=too-many-instance-attri :type uri: str """ - EXTRA_BASE64_KEY = '__extra__' + EXTRA_KEY = '__extra__' __tablename__ = "connection" @@ -165,8 +164,8 @@ def _parse_from_uri(self, uri: str): self.port = uri_parts.port if uri_parts.query: query = dict(parse_qsl(uri_parts.query, keep_blank_values=True)) - if self.EXTRA_BASE64_KEY in query: - self.extra = urlsafe_b64decode(query[self.EXTRA_BASE64_KEY]).decode('utf-8') + if self.EXTRA_KEY in query: + self.extra = query[self.EXTRA_KEY] else: self.extra = json.dumps(query) @@ -209,7 +208,7 @@ def get_uri(self) -> str: if query and self.extra_dejson == dict(parse_qsl(query, keep_blank_values=True)): uri += '?' + query else: - uri += '?' + urlencode({self.EXTRA_BASE64_KEY: urlsafe_b64encode(self.extra.encode('utf-8'))}) + uri += '?' + urlencode({self.EXTRA_KEY: self.extra}) return uri diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index a23c581f35fbc..305a19a3f3f4d 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -278,7 +278,7 @@ Encoding arbitrary json ^^^^^^^^^^^^^^^^^^^^^^^ Some json structures cannot be urlencoded without loss. For such jsons, ``get_uri`` -will encode the json as base64 and store under the url query param ``__extra__``. +will store the entire string under the url query param ``__extra__``. For example: @@ -296,7 +296,7 @@ For example: >>> ) >>> uri = c.get_uri() >>> uri - 'scheme://user:password@host%2Flocation:1234/schema?__extra__=eyJteV92YWwiOiBbImxpc3QiLCAib2YiLCAidmFsdWVzIl0sICJleHRyYSI6IHsibmVzdGVkIjogeyJqc29uIjogInZhbCJ9fX0%3D' + 'scheme://user:password@host%2Flocation:1234/schema?__extra__=%7B%22my_val%22%3A+%5B%22list%22%2C+%22of%22%2C+%22values%22%5D%2C+%22extra%22%3A+%7B%22nested%22%3A+%7B%22json%22%3A+%22val%22%7D%7D%7D' And we can verify that it returns the same dictionary: diff --git a/tests/models/test_connection.py b/tests/models/test_connection.py index 058225ff0a076..21fb7aad618b2 100644 --- a/tests/models/test_connection.py +++ b/tests/models/test_connection.py @@ -138,8 +138,7 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): description='with extras', ), UriTestCaseConfig( - test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' - '__extra__=InNpbmdsZSB2YWx1ZSI%3D', + test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' '__extra__=single+value', test_conn_attributes=dict( conn_type='scheme', host='host/location', @@ -147,13 +146,13 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): login='user', password='password', port=1234, - extra_dejson='single value', + extra='single value', ), description='with extras single value', ), UriTestCaseConfig( test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' - '__extra__=YXJiaXRyYXJ5IHN0cmluZyAqKSok', + '__extra__=arbitrary+string+%2A%29%2A%24', test_conn_attributes=dict( conn_type='scheme', host='host/location', @@ -167,7 +166,7 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): ), UriTestCaseConfig( test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' - '__extra__=WyJsaXN0IiwgIm9mIiwgInZhbHVlcyJd', + '__extra__=%5B%22list%22%2C+%22of%22%2C+%22values%22%5D', test_conn_attributes=dict( conn_type='scheme', host='host/location', @@ -181,7 +180,7 @@ def test_connection_extra_with_encryption_rotate_fernet_key(self): ), UriTestCaseConfig( test_conn_uri='scheme://user:password@host%2Flocation:1234/schema?' - '__extra__=eyJteV92YWwiOiBbImxpc3QiLCAib2YiLCAidmFsdWVzIl0sICJleHRyYSI6IHsibmVzdGVkIjogeyJqc29uIjogInZhbCJ9fX0%3D', # noqa: E501 # pylint: disable=C0301 + '__extra__=%7B%22my_val%22%3A+%5B%22list%22%2C+%22of%22%2C+%22values%22%5D%2C+%22extra%22%3A+%7B%22nested%22%3A+%7B%22json%22%3A+%22val%22%7D%7D%7D', # noqa: E501 # pylint: disable=C0301 test_conn_attributes=dict( conn_type='scheme', host='host/location', From beaa48697170dd99bfb41453404e4b940cd4b489 Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Wed, 31 Mar 2021 23:10:06 -0700 Subject: [PATCH 4/9] fix local filesystem test and spelling of json --- docs/apache-airflow/howto/connection.rst | 2 +- tests/secrets/test_local_filesystem.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index 305a19a3f3f4d..ee24952b3083f 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -277,7 +277,7 @@ Additionally, if you have created a connection, you can use ``airflow connection Encoding arbitrary json ^^^^^^^^^^^^^^^^^^^^^^^ -Some json structures cannot be urlencoded without loss. For such jsons, ``get_uri`` +Some json structures cannot be urlencoded without loss. For such json, ``get_uri`` will store the entire string under the url query param ``__extra__``. For example: diff --git a/tests/secrets/test_local_filesystem.py b/tests/secrets/test_local_filesystem.py index 93e83abc7d1e8..067b02c2f0037 100644 --- a/tests/secrets/test_local_filesystem.py +++ b/tests/secrets/test_local_filesystem.py @@ -222,9 +222,9 @@ def test_missing_file(self, mock_exists): { "conn_a": "mysql://hosta", "conn_b": ''.join( - """scheme://Login:None@host:1234/lschema? - extra__google_cloud_platform__keyfile_dict=%7B%27a%27%3A+%27b%27%7D - &extra__google_cloud_platform__keyfile_path=asaa""".split() + """scheme://Login:None@host:1234/lschema?__extra__=%7B + %22extra__google_cloud_platform__keyfile_dict%22%3A+%7B%22a%22%3A+%22b%22%7D%2C + +%22extra__google_cloud_platform__keyfile_path%22%3A+%22asaa%22%7D""".split() ), }, ), From 7d49cad94549cdeea439fb7261ea297f662d10f6 Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Thu, 1 Apr 2021 09:41:01 -0700 Subject: [PATCH 5/9] Update docs/apache-airflow/howto/connection.rst Co-authored-by: Ash Berlin-Taylor --- docs/apache-airflow/howto/connection.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index ee24952b3083f..23c1692b6c855 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -274,7 +274,7 @@ Additionally, if you have created a connection, you can use ``airflow connection .. _manage-connections-connection-types: -Encoding arbitrary json +Encoding arbitrary JSON ^^^^^^^^^^^^^^^^^^^^^^^ Some json structures cannot be urlencoded without loss. For such json, ``get_uri`` From 6803a42e2697960616ab9440457a0fba31d0c132 Mon Sep 17 00:00:00 2001 From: Daniel Standish <15932138+dstandish@users.noreply.github.com> Date: Thu, 1 Apr 2021 09:41:10 -0700 Subject: [PATCH 6/9] Update docs/apache-airflow/howto/connection.rst Co-authored-by: Ash Berlin-Taylor --- docs/apache-airflow/howto/connection.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index 23c1692b6c855..3b783a537293f 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -277,7 +277,7 @@ Additionally, if you have created a connection, you can use ``airflow connection Encoding arbitrary JSON ^^^^^^^^^^^^^^^^^^^^^^^ -Some json structures cannot be urlencoded without loss. For such json, ``get_uri`` +Some JSON structures cannot be urlencoded without loss. For such JSON, ``get_uri`` will store the entire string under the url query param ``__extra__``. For example: From 2b68866ff29b03559cbf58fd146dae01dcbb2381 Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Thu, 1 Apr 2021 10:40:49 -0700 Subject: [PATCH 7/9] spelling --- docs/apache-airflow/howto/connection.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apache-airflow/howto/connection.rst b/docs/apache-airflow/howto/connection.rst index 3b783a537293f..cbaecf2a6719f 100644 --- a/docs/apache-airflow/howto/connection.rst +++ b/docs/apache-airflow/howto/connection.rst @@ -308,7 +308,7 @@ And we can verify that it returns the same dictionary: True -But for the most common case of storing only key-value pairs, plain urlencoding is used. +But for the most common case of storing only key-value pairs, plain url encoding is used. You can verify a URI is parsed correctly like so: From 7c6a02ae670ef5d87fedeb90e2f32598dbba0c5c Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Sat, 3 Apr 2021 16:16:29 -0700 Subject: [PATCH 8/9] use json for local filesystem test --- tests/secrets/test_local_filesystem.py | 35 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/secrets/test_local_filesystem.py b/tests/secrets/test_local_filesystem.py index 067b02c2f0037..c53eb49388567 100644 --- a/tests/secrets/test_local_filesystem.py +++ b/tests/secrets/test_local_filesystem.py @@ -204,7 +204,10 @@ def test_missing_file(self, mock_exists): @parameterized.expand( ( - ("""CONN_A: 'mysql://host_a'""", {"CONN_A": "mysql://host_a"}), + ( + """CONN_A: 'mysql://host_a'""", + {"CONN_A": {'conn_type': 'mysql', 'host': 'host_a'}}, + ), ( """ conn_a: mysql://hosta @@ -220,24 +223,30 @@ def test_missing_file(self, mock_exists): a: b extra__google_cloud_platform__keyfile_path: asaa""", { - "conn_a": "mysql://hosta", - "conn_b": ''.join( - """scheme://Login:None@host:1234/lschema?__extra__=%7B - %22extra__google_cloud_platform__keyfile_dict%22%3A+%7B%22a%22%3A+%22b%22%7D%2C - +%22extra__google_cloud_platform__keyfile_path%22%3A+%22asaa%22%7D""".split() - ), + "conn_a": {'conn_type': 'mysql', 'host': 'hosta'}, + "conn_b": { + 'conn_type': 'scheme', + 'host': 'host', + 'schema': 'lschema', + 'login': 'Login', + 'password': 'None', + 'port': 1234, + 'extra_dejson': { + 'extra__google_cloud_platform__keyfile_dict': {'a': 'b'}, + 'extra__google_cloud_platform__keyfile_path': 'asaa', + }, + }, }, ), ) ) - def test_yaml_file_should_load_connection(self, file_content, expected_connection_uris): + def test_yaml_file_should_load_connection(self, file_content, expected_attrs_dict): with mock_local_file(file_content): connections_by_conn_id = local_filesystem.load_connections_dict("a.yaml") - connection_uris_by_conn_id = { - conn_id: connection.get_uri() for conn_id, connection in connections_by_conn_id.items() - } - - assert expected_connection_uris == connection_uris_by_conn_id + for conn_id, connection in connections_by_conn_id.items(): + expected_attrs = expected_attrs_dict[conn_id] + actual_attrs = {k: getattr(connection, k) for k in expected_attrs.keys()} + assert actual_attrs == expected_attrs @parameterized.expand( ( From d45157e77b941d75f2897118eaf182d76f07e491 Mon Sep 17 00:00:00 2001 From: Daniel Standish Date: Sat, 3 Apr 2021 17:13:50 -0700 Subject: [PATCH 9/9] illustrate usage of object vs string --- tests/secrets/test_local_filesystem.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/secrets/test_local_filesystem.py b/tests/secrets/test_local_filesystem.py index c53eb49388567..1c6c5aef62e8a 100644 --- a/tests/secrets/test_local_filesystem.py +++ b/tests/secrets/test_local_filesystem.py @@ -219,8 +219,9 @@ def test_missing_file(self, mock_exists): password: None port: 1234 extra_dejson: - extra__google_cloud_platform__keyfile_dict: - a: b + arbitrary_dict: + a: b + extra__google_cloud_platform__keyfile_dict: '{"a": "b"}' extra__google_cloud_platform__keyfile_path: asaa""", { "conn_a": {'conn_type': 'mysql', 'host': 'hosta'}, @@ -232,7 +233,8 @@ def test_missing_file(self, mock_exists): 'password': 'None', 'port': 1234, 'extra_dejson': { - 'extra__google_cloud_platform__keyfile_dict': {'a': 'b'}, + 'arbitrary_dict': {"a": "b"}, + 'extra__google_cloud_platform__keyfile_dict': '{"a": "b"}', 'extra__google_cloud_platform__keyfile_path': 'asaa', }, },