Repository: great-expectations/great_expectations

Issue:
Data Docs > S3 > Links to runs are access Denied
Hi All!

When I use GE to create data docs through either `docs build` or the API `context.build_data_docs()` I find that the created files arrive fine in S3, however the index.html page does not link to them properly and I get 403 S3 errors. 

I find that if I navigate to the created html in the docs/validations prefix on S3 it is fine. After comparing the link provided by the index.html and the direct link available in S3 UI - they are identical except that the one from S3 is url encoded.

This is otherwise fine if running locally.

```yaml
data_docs_sites:
  s3_site:
    class_name: SiteBuilder
    store_backend:
      class_name: TupleS3StoreBackend
      bucket: mybucket.org.com
      prefix: docs/
```

Cheers, 
Data Docs > S3 > Links to runs are access Denied
Hi All!

When I use GE to create data docs through either `docs build` or the API `context.build_data_docs()` I find that the created files arrive fine in S3, however the index.html page does not link to them properly and I get 403 S3 errors. 

I find that if I navigate to the created html in the docs/validations prefix on S3 it is fine. After comparing the link provided by the index.html and the direct link available in S3 UI - they are identical except that the one from S3 is url encoded.

This is otherwise fine if running locally.

```yaml
data_docs_sites:
  s3_site:
    class_name: SiteBuilder
    store_backend:
      class_name: TupleS3StoreBackend
      bucket: mybucket.org.com
      prefix: docs/
```

Cheers, 


Discussion:
@troxil thanks for the report. Could you please provide me with two example URLs so I can reproduce this? If there is anything sensitive in the url, just change them both to something like `mybucket.org.com`
I think I'm also experiencing the same problem. 

Not able to view the Expectation Suites or Validation Runs on this path: 
https://s3.console.aws.amazon.com/s3/object/bucket/prefix/data_docs/index.html
Same files can be viewed from this subdirectory (giving validations as an example)
https://s3.console.aws.amazon.com/s3/object/bucket/prefix/data_docs/validations/xxx/xxx.html
hey @Aylr 

Here's the two.

**Link from index.html**

```text
https://s3-ap-southeast-2.amazonaws.com/datadocs.org.com/validations/path/airflow-scheduled__2020-04-06T03:00:00+00:00-2020-04-07%2012:57:18.956372+00:00/ge_batch_id=24e73984-78d1-11ea-beee-066d964bd674.html
```

**Link if you navigate to object in S3 view it's Object URL**
```text
https://s3-ap-southeast-2.amazonaws.com/datadocs.org.com/validations/path/airflow-scheduled__2020-04-05T03%3A00%3A00%2B00%3A00-2020-04-07+08%3A43%3A17.097853%2B00%3A00/ge_batch_id%3D036e02a8-78ac-11ea-a3c2-066d964bd674.html
```

### Problem area
L139 `great_expectations/render/renderer/site_index_page_renderer.py`
The filepath isn't encoded but could be, perhaps with something like `urllib.parse.quote()`

````python
#... snip
                        "tag": "a",
                        "styling": {
                            "attributes": {
                                "href": link_dict["filepath"]
                            },
#... snip



@troxil Super helpful examples and code snippet. Do you want to take a crack at submitting a PR for this?
@troxil thanks for the report. Could you please provide me with two example URLs so I can reproduce this? If there is anything sensitive in the url, just change them both to something like `mybucket.org.com`
I think I'm also experiencing the same problem. 

Not able to view the Expectation Suites or Validation Runs on this path: 
https://s3.console.aws.amazon.com/s3/object/bucket/prefix/data_docs/index.html
Same files can be viewed from this subdirectory (giving validations as an example)
https://s3.console.aws.amazon.com/s3/object/bucket/prefix/data_docs/validations/xxx/xxx.html
hey @Aylr 

Here's the two.

**Link from index.html**

```text
https://s3-ap-southeast-2.amazonaws.com/datadocs.org.com/validations/path/airflow-scheduled__2020-04-06T03:00:00+00:00-2020-04-07%2012:57:18.956372+00:00/ge_batch_id=24e73984-78d1-11ea-beee-066d964bd674.html
```

**Link if you navigate to object in S3 view it's Object URL**
```text
https://s3-ap-southeast-2.amazonaws.com/datadocs.org.com/validations/path/airflow-scheduled__2020-04-05T03%3A00%3A00%2B00%3A00-2020-04-07+08%3A43%3A17.097853%2B00%3A00/ge_batch_id%3D036e02a8-78ac-11ea-a3c2-066d964bd674.html
```

### Problem area
L139 `great_expectations/render/renderer/site_index_page_renderer.py`
The filepath isn't encoded but could be, perhaps with something like `urllib.parse.quote()`

````python
#... snip
                        "tag": "a",
                        "styling": {
                            "attributes": {
                                "href": link_dict["filepath"]
                            },
#... snip



@troxil Super helpful examples and code snippet. Do you want to take a crack at submitting a PR for this?

Resolving patch:
diff --git a/great_expectations/render/renderer/site_index_page_renderer.py b/great_expectations/render/renderer/site_index_page_renderer.py
--- a/great_expectations/render/renderer/site_index_page_renderer.py
+++ b/great_expectations/render/renderer/site_index_page_renderer.py
@@ -1,9 +1,11 @@
 import logging
+import urllib
 from collections import OrderedDict
 
 import traceback
 
-from .renderer import Renderer
+from great_expectations.render.renderer.call_to_action_renderer import CallToActionRenderer
+from great_expectations.render.renderer.renderer import Renderer
 from great_expectations.render.types import (
     RenderedSectionContent,
     RenderedDocumentContent,
@@ -12,9 +14,6 @@
     RenderedTableContent,
     RenderedBulletListContent,
 )
-from great_expectations.exceptions import GreatExpectationsError
-
-from .call_to_action_renderer import CallToActionRenderer
 
 logger = logging.getLogger(__name__)
 
@@ -49,7 +48,7 @@ def _generate_links_table_rows(cls, index_links_dict, link_list_keys_to_render):
                         "tag": "a",
                         "styling": {
                             "attributes": {
-                                "href": expectation_suite_link_dict["filepath"]
+                                "href": urllib.parse.quote(expectation_suite_link_dict["filepath"])
                             },
                             "classes": ["ge-index-page-table-expectation-suite-link"]
                         }
@@ -81,7 +80,7 @@ def _generate_links_table_rows(cls, index_links_dict, link_list_keys_to_render):
                                 "tag": "a",
                                 "styling": {
                                     "attributes": {
-                                        "href": link_dict["filepath"]
+                                        "href": urllib.parse.quote(link_dict["filepath"])
                                     },
                                     "params": {
                                         "validation_success": {
@@ -135,7 +134,7 @@ def _generate_links_table_rows(cls, index_links_dict, link_list_keys_to_render):
                         "tag": "a",
                         "styling": {
                             "attributes": {
-                                "href": link_dict["filepath"]
+                                "href": urllib.parse.quote(link_dict["filepath"])
                             },
                             "params": {
                                 "validation_success": {
@@ -175,7 +174,6 @@ def _generate_links_table_rows(cls, index_links_dict, link_list_keys_to_render):
 
     @classmethod
     def render(cls, index_links_dict):
-
         sections = []
         cta_object = index_links_dict.pop("cta_object", None)
 
@@ -272,7 +270,7 @@ def render(cls, index_links_dict):
                                     "tag": "a",
                                     "styling": {
                                         "attributes": {
-                                            "href": profiling_link_dict["filepath"]
+                                            "href": urllib.parse.quote(profiling_link_dict["filepath"])
                                         },
                                         "classes": ["ge-index-page-table-expectation-suite-link"]
                                     }


Test patch:
diff --git a/tests/datasource/test_sparkdf_datasource.py b/tests/datasource/test_sparkdf_datasource.py
--- a/tests/datasource/test_sparkdf_datasource.py
+++ b/tests/datasource/test_sparkdf_datasource.py
@@ -266,6 +266,7 @@ def test_invalid_reader_sparkdf_datasource(tmp_path_factory):
 
 
 def test_spark_config():
+    pytest.importorskip("pyspark")
     source = SparkDFDatasource()
     conf = source.spark.sparkContext.getConf().getAll()
     # Without specifying any spark_config values we get defaults
@@ -285,6 +286,7 @@ def test_spark_config():
 
 
 def test_pandas_datasource_processes_dataset_options(test_folder_connection_path):
+    pytest.importorskip("pyspark")
     datasource = SparkDFDatasource('PandasCSV', generators={
             "subdir_reader": {
                 "class_name": "SubdirReaderBatchKwargsGenerator",