# Buggy code
def post(self, sketch_id):
    """Handles POST request to the resource.
    Handler for /api/v1/sketches/:sketch_id/explore/

    Args:
        sketch_id: Integer primary key for a sketch database model

    Returns:
        JSON with list of matched events
    """
    sketch = Sketch.query.get_with_acl(sketch_id)
    if not sketch:
        abort(HTTP_STATUS_CODE_NOT_FOUND, "No sketch found with this ID.")

    if not sketch.has_permission(current_user, "read"):
        abort(
            HTTP_STATUS_CODE_FORBIDDEN,
            "User does not have read access controls on sketch.",
        )

    if sketch.get_status.status == "archived":
        abort(HTTP_STATUS_CODE_BAD_REQUEST, "Unable to query on an archived sketch.")

    form = forms.ExploreForm.build(request)

    if not form.validate_on_submit():
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "Unable to explore data, unable to validate form data",
        )

    # TODO: Remove form and use json instead.
    query_dsl = form.dsl.data
    enable_scroll = form.enable_scroll.data
    scroll_id = form.scroll_id.data
    file_name = form.file_name.data
    count = bool(form.count.data)

    query_filter = request.json.get("filter", {})

    return_field_string = form.fields.data
    if return_field_string:
        return_fields = [x.strip() for x in return_field_string.split(",")]
    else:
        return_fields = query_filter.get("fields", [])
        return_fields = [field["field"] for field in return_fields]
        return_fields.extend(DEFAULT_SOURCE_FIELDS)

    if not query_filter:
        query_filter = {}

    all_indices = list({t.searchindex.index_name for t in sketch.timelines})
    indices = query_filter.get("indices", all_indices)

    # If _all in indices then execute the query on all indices
    if "_all" in indices:
        indices = all_indices

    # Remove indices that don't exist from search.
    indices = utils.validate_indices(indices, self.datastore)

    # Make sure that the indices in the filter are part of the sketch.
    # This will also remove any deleted timeline from the search result.
    indices, timeline_ids = get_validated_indices(indices, sketch)

    if not indices:
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "No valid search indices were found to perform the search on.",
        )

    # Make sure we have a query string or star filter
    if not (
        form.query.data,
        query_filter.get("star"),
        query_filter.get("events"),
        query_dsl,
    ):
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "The request needs a query string/DSL and or a star filter.",
        )

    # Aggregate hit count per index.
    index_stats_agg = {
        "indices": {"terms": {"field": "_index", "min_doc_count": 0}},
        "timelines": {"terms": {"field": "__ts_timeline_id", "min_doc_count": 0}},
    }
    if count:
        # Count operations do not support size parameters.
        if "size" in query_filter:
            _ = query_filter.pop("size")
        if "terminate_after" in query_filter:
            _ = query_filter.pop("terminate_after")

        try:
            result = self.datastore.search(
                sketch_id=sketch_id,
                query_string=form.query.data,
                query_filter=query_filter,
                query_dsl=query_dsl,
                indices=indices,
                timeline_ids=timeline_ids,
                count=True,
            )
        except ValueError as e:
            abort(HTTP_STATUS_CODE_BAD_REQUEST, e)

        # Get number of matching documents per index.
        schema = {"meta": {"total_count": result}, "objects": []}
        return jsonify(schema)

    if file_name:
        file_object = io.BytesIO()

        form_data = {
            "created_at": datetime.datetime.utcnow().isoformat(),
            "created_by": current_user.username,
            "sketch": sketch_id,
            "query": form.query.data,
            "query_dsl": query_dsl,
            "query_filter": query_filter,
            "return_fields": return_fields,
        }
        with zipfile.ZipFile(file_object, mode="w") as zip_file:
            zip_file.writestr("METADATA", data=json.dumps(form_data))
            fh = export.query_to_filehandle(
                query_string=form.query.data,
                query_dsl=query_dsl,
                query_filter=query_filter,
                indices=indices,
                sketch=sketch,
                datastore=self.datastore,
            )
            fh.seek(0)
            zip_file.writestr("query_results.csv", fh.read())
        file_object.seek(0)
        return send_file(file_object, mimetype="zip", attachment_filename=file_name)

    if scroll_id:
        # pylint: disable=unexpected-keyword-arg
        result = self.datastore.client.scroll(scroll_id=scroll_id, scroll="1m")
    else:
        try:
            result = self.datastore.search(
                sketch_id=sketch_id,
                query_string=form.query.data,
                query_filter=query_filter,
                query_dsl=query_dsl,
                indices=indices,
                aggregations=index_stats_agg,
                return_fields=return_fields,
                enable_scroll=enable_scroll,
                timeline_ids=timeline_ids,
            )
        except ValueError as e:
            abort(HTTP_STATUS_CODE_BAD_REQUEST, e)

    # Get number of matching documents per index.
    count_per_index = {}
    try:
        for bucket in result["aggregations"]["indices"]["buckets"]:
            key = bucket.get("key")
            if key:
                count_per_index[key] = bucket.get("doc_count")
    except KeyError:
        pass

    # Get number of matching documents per timeline.
    count_per_timeline = {}
    try:
        for bucket in result["aggregations"]["timelines"]["buckets"]:
            key = bucket.get("key")
            if key:
                count_per_timeline[key] = bucket.get("doc_count")
    except KeyError:
        pass

    comments = {}
    if "comment" in return_fields:
        events = Event.query.filter_by(sketch=sketch).all()
        for event in events:
            for comment in event.comments:
                comments.setdefault(event.document_id, [])
                comments[event.document_id].append(comment.comment)

    # Get labels for each event that matches the sketch.
    # Remove all other labels.
    for event in result["hits"]["hits"]:
        event["selected"] = False
        event["_source"]["label"] = []
        try:
            for label in event["_source"]["timesketch_label"]:
                if sketch.id != label["sketch_id"]:
                    continue
                event["_source"]["label"].append(label["name"])
            del event["_source"]["timesketch_label"]
        except KeyError:
            pass

        if "comment" in return_fields:
            event["_source"]["comment"] = comments.get(event["_id"], [])

    # Update or create user state view. This is used in the UI to let
    # the user get back to the last state in the explore view.
    # TODO: Add a call to utils.update_sketch_last_activity once new
    # mechanism has been added, instead of relying on user views.
    view = View.get_or_create(user=current_user, sketch=sketch, name="")
    view.update_modification_time()
    view.query_string = form.query.data
    view.query_filter = json.dumps(query_filter, ensure_ascii=False)
    view.query_dsl = json.dumps(query_dsl, ensure_ascii=False)
    db_session.add(view)
    db_session.commit()

    # Add metadata for the query result. This is used by the UI to
    # render the event correctly and to display timing and hit count
    # information.
    tl_colors = {}
    tl_names = {}
    for timeline in sketch.timelines:
        tl_colors[timeline.searchindex.index_name] = timeline.color
        tl_names[timeline.searchindex.index_name] = timeline.name

    meta = {
        "es_time": result["took"],
        "es_total_count": result["hits"]["total"],
        "timeline_colors": tl_colors,
        "timeline_names": tl_names,
        "count_per_index": count_per_index,
        "count_per_timeline": count_per_timeline,
        "scroll_id": result.get("_scroll_id", ""),
    }

    # Elasticsearch version 7.x returns total hits as a dictionary.
    # TODO: Refactor when version 6.x has been deprecated.
    if isinstance(meta["es_total_count"], dict):
        meta["es_total_count"] = meta["es_total_count"].get("value", 0)

    schema = {"meta": meta, "objects": result["hits"]["hits"]}
    return jsonify(schema)


# Traceback (urllib3.exceptions.ReadTimeoutError)
[2021-01-21 09:57:16,813] celery.worker.strategy/INFO Received task: timesketch.lib.tasks.run_csv_jsonl[34b8f46651b94a8d8ad9e29bb06a147c]
[2021-01-21 09:57:16,819] timesketch.tasks/INFO Index timeline [vol timeline] to index [34b8f46651b94a8d8ad9e29bb06a147c] (source: jsonl)
[2021-01-21 09:58:14,763] timesketch.elasticsearch/ERROR Unable to add events
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 421, in _make_request
six.raise_from(e, None)
File "<string>", line 3, in raise_from
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 416, in _make_request
httplib_response = conn.getresponse()
File "/usr/lib/python3.8/http/client.py", line 1347, in getresponse
response.begin()
File "/usr/lib/python3.8/http/client.py", line 307, in begin
version, status, reason = self._read_status()
File "/usr/lib/python3.8/http/client.py", line 268, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
File "/usr/lib/python3.8/socket.py", line 669, in readinto
return self._sock.recv_into(b)
socket.timeout: timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.8/dist-packages/elasticsearch/connection/http_urllib3.py", line 241, in perform_request
response = self.pool.urlopen(
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 719, in urlopen
retries = retries.increment(
File "/usr/lib/python3/dist-packages/urllib3/util/retry.py", line 376, in increment
raise six.reraise(type(error), error, _stacktrace)
File "/usr/local/lib/python3.8/dist-packages/six.py", line 693, in reraise
raise value
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 665, in urlopen
httplib_response = self._make_request(
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 423, in _make_request
self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
File "/usr/lib/python3/dist-packages/urllib3/connectionpool.py", line 330, in _raise_timeout
raise ReadTimeoutError(
urllib3.exceptions.ReadTimeoutError: HTTPConnectionPool(host='elasticsearch', port=9200): Read timed out. (read timeout=10)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/usr/local/lib/python3.8/dist-packages/timesketch/lib/datastores/elastic.py", line 830, in flush_queued_events
results = self.client.bulk(body=self.import_events)
File "/usr/local/lib/python3.8/dist-packages/elasticsearch/client/utils.py", line 84, in _wrapped
return func(*args, params=params, **kwargs)
File "/usr/local/lib/python3.8/dist-packages/elasticsearch/client/__init__.py", line 448, in bulk
return self.transport.perform_request(
File "/usr/local/lib/python3.8/dist-packages/elasticsearch/transport.py", line 351, in perform_request
status, headers_response, data = connection.perform_request(
File "/usr/local/lib/python3.8/dist-packages/elasticsearch/connection/http_urllib3.py", line 253, in perform_request
raise ConnectionTimeout("TIMEOUT", str(e), e)
elasticsearch.exceptions.ConnectionTimeout: ConnectionTimeout caused by - ReadTimeoutError(HTTPConnectionPool(host='elasticsearch', port=9200): Read timed out. (read timeout=10))
[2021-01-21 09:58:15,185] celery.app.trace/ERROR Task timesketch.lib.tasks.run_csv_jsonl[34b8f46651b94a8d8ad9e29bb06a147c] raised unexpected: UnboundLocalError("local variable 'results' referenced before assignment")
Traceback (most recent call last):
File "/usr/local/lib/python3.8/dist-packages/celery/app/trace.py", line 385, in trace_task
R = retval = fun(*args, **kwargs)
File "/usr/local/lib/python3.8/dist-packages/timesketch/app.py", line 198, in __call__
return TaskBase.__call__(self, *args, **kwargs)
File "/usr/local/lib/python3.8/dist-packages/celery/app/trace.py", line 650, in __protected_call__
return self.run(*args, **kwargs)
File "/usr/local/lib/python3.8/dist-packages/timesketch/lib/tasks.py", line 563, in run_csv_jsonl
results = es.flush_queued_events()
File "/usr/local/lib/python3.8/dist-packages/timesketch/lib/datastores/elastic.py", line 835, in flush_queued_events
errors_in_upload = results.get('errors', False)
UnboundLocalError: local variable 'results' referenced before assignment

# Fixed code
def post(self, sketch_id):
    """Handles POST request to the resource.
    Handler for /api/v1/sketches/:sketch_id/explore/

    Args:
        sketch_id: Integer primary key for a sketch database model

    Returns:
        JSON with list of matched events
    """
    sketch = Sketch.query.get_with_acl(sketch_id)
    if not sketch:
        abort(HTTP_STATUS_CODE_NOT_FOUND, "No sketch found with this ID.")

    if not sketch.has_permission(current_user, "read"):
        abort(
            HTTP_STATUS_CODE_FORBIDDEN,
            "User does not have read access controls on sketch.",
        )

    if sketch.get_status.status == "archived":
        abort(HTTP_STATUS_CODE_BAD_REQUEST, "Unable to query on an archived sketch.")

    form = forms.ExploreForm.build(request)

    if not form.validate_on_submit():
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "Unable to explore data, unable to validate form data",
        )

    # TODO: Remove form and use json instead.
    query_dsl = form.dsl.data
    enable_scroll = form.enable_scroll.data
    scroll_id = form.scroll_id.data
    file_name = form.file_name.data
    count = bool(form.count.data)

    query_filter = request.json.get("filter", {})

    return_field_string = form.fields.data
    if return_field_string:
        return_fields = [x.strip() for x in return_field_string.split(",")]
    else:
        return_fields = query_filter.get("fields", [])
        return_fields = [field["field"] for field in return_fields]
        return_fields.extend(DEFAULT_SOURCE_FIELDS)

    if not query_filter:
        query_filter = {}

    all_indices = list({t.searchindex.index_name for t in sketch.timelines})
    indices = query_filter.get("indices", all_indices)

    # If _all in indices then execute the query on all indices
    if "_all" in indices:
        indices = all_indices

    # Make sure that the indices in the filter are part of the sketch.
    # This will also remove any deleted timeline from the search result.
    indices, timeline_ids = get_validated_indices(indices, sketch)

    # Remove indices that don't exist from search.
    indices = utils.validate_indices(indices, self.datastore)

    if not indices:
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "No valid search indices were found to perform the search on.",
        )

    # Make sure we have a query string or star filter
    if not (
        form.query.data,
        query_filter.get("star"),
        query_filter.get("events"),
        query_dsl,
    ):
        abort(
            HTTP_STATUS_CODE_BAD_REQUEST,
            "The request needs a query string/DSL and or a star filter.",
        )

    # Aggregate hit count per index.
    index_stats_agg = {
        "indices": {"terms": {"field": "_index", "min_doc_count": 0}},
        "timelines": {"terms": {"field": "__ts_timeline_id", "min_doc_count": 0}},
    }
    if count:
        # Count operations do not support size parameters.
        if "size" in query_filter:
            _ = query_filter.pop("size")
        if "terminate_after" in query_filter:
            _ = query_filter.pop("terminate_after")

        try:
            result = self.datastore.search(
                sketch_id=sketch_id,
                query_string=form.query.data,
                query_filter=query_filter,
                query_dsl=query_dsl,
                indices=indices,
                timeline_ids=timeline_ids,
                count=True,
            )
        except ValueError as e:
            abort(HTTP_STATUS_CODE_BAD_REQUEST, e)

        # Get number of matching documents per index.
        schema = {"meta": {"total_count": result}, "objects": []}
        return jsonify(schema)

    if file_name:
        file_object = io.BytesIO()

        form_data = {
            "created_at": datetime.datetime.utcnow().isoformat(),
            "created_by": current_user.username,
            "sketch": sketch_id,
            "query": form.query.data,
            "query_dsl": query_dsl,
            "query_filter": query_filter,
            "return_fields": return_fields,
        }
        with zipfile.ZipFile(file_object, mode="w") as zip_file:
            zip_file.writestr("METADATA", data=json.dumps(form_data))
            fh = export.query_to_filehandle(
                query_string=form.query.data,
                query_dsl=query_dsl,
                query_filter=query_filter,
                indices=indices,
                sketch=sketch,
                datastore=self.datastore,
            )
            fh.seek(0)
            zip_file.writestr("query_results.csv", fh.read())
        file_object.seek(0)
        return send_file(file_object, mimetype="zip", attachment_filename=file_name)

    if scroll_id:
        # pylint: disable=unexpected-keyword-arg
        result = self.datastore.client.scroll(scroll_id=scroll_id, scroll="1m")
    else:
        try:
            result = self.datastore.search(
                sketch_id=sketch_id,
                query_string=form.query.data,
                query_filter=query_filter,
                query_dsl=query_dsl,
                indices=indices,
                aggregations=index_stats_agg,
                return_fields=return_fields,
                enable_scroll=enable_scroll,
                timeline_ids=timeline_ids,
            )
        except ValueError as e:
            abort(HTTP_STATUS_CODE_BAD_REQUEST, e)

    # Get number of matching documents per index.
    count_per_index = {}
    try:
        for bucket in result["aggregations"]["indices"]["buckets"]:
            key = bucket.get("key")
            if key:
                count_per_index[key] = bucket.get("doc_count")
    except KeyError:
        pass

    # Get number of matching documents per timeline.
    count_per_timeline = {}
    try:
        for bucket in result["aggregations"]["timelines"]["buckets"]:
            key = bucket.get("key")
            if key:
                count_per_timeline[key] = bucket.get("doc_count")
    except KeyError:
        pass

    comments = {}
    if "comment" in return_fields:
        events = Event.query.filter_by(sketch=sketch).all()
        for event in events:
            for comment in event.comments:
                comments.setdefault(event.document_id, [])
                comments[event.document_id].append(comment.comment)

    # Get labels for each event that matches the sketch.
    # Remove all other labels.
    for event in result["hits"]["hits"]:
        event["selected"] = False
        event["_source"]["label"] = []
        try:
            for label in event["_source"]["timesketch_label"]:
                if sketch.id != label["sketch_id"]:
                    continue
                event["_source"]["label"].append(label["name"])
            del event["_source"]["timesketch_label"]
        except KeyError:
            pass

        if "comment" in return_fields:
            event["_source"]["comment"] = comments.get(event["_id"], [])

    # Update or create user state view. This is used in the UI to let
    # the user get back to the last state in the explore view.
    # TODO: Add a call to utils.update_sketch_last_activity once new
    # mechanism has been added, instead of relying on user views.
    view = View.get_or_create(user=current_user, sketch=sketch, name="")
    view.update_modification_time()
    view.query_string = form.query.data
    view.query_filter = json.dumps(query_filter, ensure_ascii=False)
    view.query_dsl = json.dumps(query_dsl, ensure_ascii=False)
    db_session.add(view)
    db_session.commit()

    # Add metadata for the query result. This is used by the UI to
    # render the event correctly and to display timing and hit count
    # information.
    tl_colors = {}
    tl_names = {}
    for timeline in sketch.timelines:
        tl_colors[timeline.searchindex.index_name] = timeline.color
        tl_names[timeline.searchindex.index_name] = timeline.name

    meta = {
        "es_time": result["took"],
        "es_total_count": result["hits"]["total"],
        "timeline_colors": tl_colors,
        "timeline_names": tl_names,
        "count_per_index": count_per_index,
        "count_per_timeline": count_per_timeline,
        "scroll_id": result.get("_scroll_id", ""),
    }

    # Elasticsearch version 7.x returns total hits as a dictionary.
    # TODO: Refactor when version 6.x has been deprecated.
    if isinstance(meta["es_total_count"], dict):
        meta["es_total_count"] = meta["es_total_count"].get("value", 0)

    schema = {"meta": meta, "objects": result["hits"]["hits"]}
    return jsonify(schema)