diff --git a/airflow_code_editor/app_builder_view.py b/airflow_code_editor/app_builder_view.py index e33c588..f9c019e 100644 --- a/airflow_code_editor/app_builder_view.py +++ b/airflow_code_editor/app_builder_view.py @@ -73,15 +73,15 @@ def load(self, path=None): def format(self): return self._format() - @expose("/tree", methods=["GET"]) + @expose("/tree", methods=["GET", "HEAD"]) @auth.has_access(PERMISSIONS) def tree_base(self, path=None): - return self._tree(path, args=request.args) + return self._tree(path, args=request.args, method=request.method) - @expose("/tree/", methods=["GET"]) + @expose("/tree/", methods=["GET", "HEAD"]) @auth.has_access(PERMISSIONS) def tree(self, path=None): - return self._tree(path, args=request.args) + return self._tree(path, args=request.args, method=request.method) @expose("/ping", methods=["GET"]) @auth.has_access(PERMISSIONS) @@ -143,12 +143,12 @@ def format(self): @expose("/tree", methods=["GET"]) @has_dag_access(can_dag_edit=True) def tree_base(self, path=None): - return self._tree(path) + return self._tree(path, args=request.args, method=request.method) @expose("/tree/", methods=["GET"]) @has_dag_access(can_dag_edit=True) def tree(self, path=None): - return self._tree(path) + return self._tree(path, args=request.args, method=request.method) def _render(self, template, *args, **kargs): return self.render_template( diff --git a/airflow_code_editor/code_editor_view.py b/airflow_code_editor/code_editor_view.py index bf94a3d..b7a3036 100644 --- a/airflow_code_editor/code_editor_view.py +++ b/airflow_code_editor/code_editor_view.py @@ -20,8 +20,8 @@ from flask import request, make_response from flask_wtf.csrf import generate_csrf from airflow.version import version -from airflow_code_editor.commons import HTTP_404_NOT_FOUND -from airflow_code_editor.tree import get_tree +from airflow_code_editor.commons import HTTP_200_OK, HTTP_404_NOT_FOUND +from airflow_code_editor.tree import get_tree, get_stat from airflow_code_editor.utils import ( get_plugin_boolean_config, get_plugin_int_config, @@ -63,9 +63,7 @@ def _save(self, path=None): logging.error(ex) return prepare_api_response( path=normalize_path(path), - error_message="Error saving {path}: {message}".format( - path=path, message=error_message(ex) - ), + error_message="Error saving {path}: {message}".format(path=path, message=error_message(ex)), ) def _git_repo(self, path): @@ -84,9 +82,7 @@ def _git_repo_get(self, path): attachment_filename = None response = execute_git_command(["cat-file", "-p", path]).prepare_git_response() if attachment_filename: - content_disposition = 'attachment; filename="{0}"'.format( - attachment_filename - ) + content_disposition = 'attachment; filename="{0}"'.format(attachment_filename) response.headers["Content-Disposition"] = content_disposition try: content_type = mimetypes.guess_type(attachment_filename)[0] @@ -140,15 +136,19 @@ def _format(self): ) except Exception as ex: logging.error(ex) - return prepare_api_response( - error_message="Error formatting: {message}".format( - message=error_message(ex) - ) - ) + return prepare_api_response(error_message="Error formatting: {message}".format(message=error_message(ex))) - def _tree(self, path, args={}): + def _tree(self, path, args={}, method="GET"): try: - return prepare_api_response(value=get_tree(path, args)) + if method == "HEAD": + stat = get_stat(path) + response = make_response("OK", HTTP_200_OK) + response.headers["X-Id"] = stat["id"] + response.headers["X-Leaf"] = "true" if stat["leaf"] else "false" + response.headers["X-Exists"] = "true" if stat["exists"] else "false" + return response + else: + return prepare_api_response(value=get_tree(path, args)) except Exception as ex: logging.error(ex) return prepare_api_response( diff --git a/airflow_code_editor/flask_admin_view.py b/airflow_code_editor/flask_admin_view.py index 1ff8d09..83e4364 100644 --- a/airflow_code_editor/flask_admin_view.py +++ b/airflow_code_editor/flask_admin_view.py @@ -76,15 +76,15 @@ def load(self, path=None): def format(self, path=None): return self._load(path) - @expose("/tree", methods=["GET"]) + @expose("/tree", methods=["GET", "HEAD"]) @login_required def tree_base(self, path=None): - return self._tree(path, args=request.args) + return self._tree(path, args=request.args, method=request.method) - @expose("/tree/", methods=["GET"]) + @expose("/tree/", methods=["GET", "HEAD"]) @login_required def tree(self, path=None): - return self._tree(path, args=request.args) + return self._tree(path, args=request.args, method=request.method) @expose("/ping", methods=["GET"]) @login_required diff --git a/airflow_code_editor/tree.py b/airflow_code_editor/tree.py index 4f99c59..01c6ee2 100644 --- a/airflow_code_editor/tree.py +++ b/airflow_code_editor/tree.py @@ -15,6 +15,7 @@ # limitations under the Licens import re +import fs from datetime import datetime from typing import Any, Callable, Dict, List, NamedTuple, Optional from airflow_code_editor.commons import ( @@ -39,7 +40,7 @@ ) from airflow_code_editor.fs import RootFS -__all__ = ['get_tree'] +__all__ = ['get_tree', 'get_stat'] class NodeDef(NamedTuple): @@ -78,9 +79,7 @@ def get_root_node(path: Optional[str], args: Args) -> TreeOutput: if id_ is None or not node.condition(): continue # Add the node - result.append( - {'id': id_, 'label': node.label, 'leaf': node.leaf, 'icon': node.icon} - ) + result.append({'id': id_, 'label': node.label, 'leaf': node.leaf, 'icon': node.icon}) # If the node is files, add the mount points if id_ == 'files': mount_points = read_mount_points_config() @@ -115,9 +114,7 @@ def get_files_node(path: Optional[str], args: Args) -> TreeOutput: 'leaf': leaf, 'size': size, 'mode': s.st_mode, - 'mtime': datetime.fromtimestamp(int(s.st_mtime)).isoformat() - if s.st_mtime - else None, + 'mtime': datetime.fromtimestamp(int(s.st_mtime)).isoformat() if s.st_mtime else None, } ) else: # Short format @@ -216,3 +213,17 @@ def get_tree(path: Optional[str] = None, args: Args = {}) -> TreeOutput: return [] # Execute node function return TREE_NODES[root].get_children(path_argv, args) + + +def get_stat(path: Optional[str] = None, args: Args = {}) -> TreeOutput: + "Get stat for the given path" + try: + if not path: + return {'id': path, 'leaf': False, 'exists': True} + path = normalize_path(path) + get_tree(path, args) + return {'id': path, 'leaf': False, 'exists': True} + except fs.errors.DirectoryExpected: + return {'id': path, 'leaf': True, 'exists': True} + except fs.errors.ResourceNotFound: + return {'id': path, 'leaf': None, 'exists': False} diff --git a/airflow_code_editor/utils.py b/airflow_code_editor/utils.py index b4c3414..802db7a 100644 --- a/airflow_code_editor/utils.py +++ b/airflow_code_editor/utils.py @@ -70,9 +70,7 @@ def get_plugin_boolean_config(key: str) -> bool: "Get a plugin boolean configuration/default for a given key" return cast( bool, - configuration.conf.getboolean( - PLUGIN_NAME, key, fallback=PLUGIN_DEFAULT_CONFIG[key] - ), + configuration.conf.getboolean(PLUGIN_NAME, key, fallback=PLUGIN_DEFAULT_CONFIG[key]), ) # type: ignore @@ -80,9 +78,7 @@ def get_plugin_int_config(key: str) -> int: "Get a plugin int configuration/default for a given key" return cast( int, - configuration.conf.getint( - PLUGIN_NAME, key, fallback=PLUGIN_DEFAULT_CONFIG[key] - ), + configuration.conf.getint(PLUGIN_NAME, key, fallback=PLUGIN_DEFAULT_CONFIG[key]), ) # type: ignore @@ -94,8 +90,7 @@ def is_enabled() -> bool: def get_root_folder() -> Path: "Return the configured root folder or Airflow DAGs folder" return Path( - get_plugin_config('root_directory') - or cast(str, configuration.conf.get('core', 'dags_folder')) # type: ignore + get_plugin_config('root_directory') or cast(str, configuration.conf.get('core', 'dags_folder')) # type: ignore ).resolve() @@ -141,17 +136,13 @@ def read_mount_points_config() -> Dict[str, MountPoint]: else: suffix = str(i) try: - if not configuration.conf.has_option( - PLUGIN_NAME, 'mount{}_name'.format(suffix) - ): + if not configuration.conf.has_option(PLUGIN_NAME, 'mount{}_name'.format(suffix)): break except Exception: # backports.configparser.NoSectionError and friends break name = configuration.conf.get(PLUGIN_NAME, 'mount{}_name'.format(suffix)) path = configuration.conf.get(PLUGIN_NAME, 'mount{}_path'.format(suffix)) - config[name] = MountPoint( - path=path, default=mount_conf['name'] == ROOT_MOUNTPOUNT - ) + config[name] = MountPoint(path=path, default=mount_conf['name'] == ROOT_MOUNTPOUNT) return config diff --git a/tests/test_tree.py b/tests/test_tree.py index 00e6e9b..18d4edb 100644 --- a/tests/test_tree.py +++ b/tests/test_tree.py @@ -21,14 +21,14 @@ class TestTree(TestCase): def setUp(self): self.root_dir = Path(__file__).parent - configuration.conf.set(PLUGIN_NAME, 'git_init_repo', 'False') - configuration.conf.set(PLUGIN_NAME, 'root_directory', str(self.root_dir)) + configuration.conf.set(PLUGIN_NAME, "git_init_repo", "False") + configuration.conf.set(PLUGIN_NAME, "root_directory", str(self.root_dir)) def test_tree(self): with app.app_context(): t = get_tree() assert len(t) > 0 - assert 'git' in (x['id'] for x in t) + assert "git" in (x["id"] for x in t) def test_tags(self): with app.app_context(): @@ -48,28 +48,28 @@ def test_remote_branches(self): def test_files(self): with app.app_context(): t = get_tree("files") - assert len([x.get('id') for x in t if x.get('id') == 'test_utils.py']) == 1 + assert len([x.get("id") for x in t if x.get("id") == "test_utils.py"]) == 1 t = get_tree("files/folder") - assert len([x.get('id') for x in t if x.get('id') == '1']) == 1 + assert len([x.get("id") for x in t if x.get("id") == "1"]) == 1 def test_files_long(self): with app.app_context(): t = get_tree("files", ["long"]) - assert len([x.get('id') for x in t if x.get('id') == 'folder']) == 1 - folder = [x for x in t if x.get('id') == 'folder'][0] - assert not folder['leaf'] - assert folder['size'] == 3 - assert stat.S_ISDIR(folder['mode']) + assert len([x.get("id") for x in t if x.get("id") == "folder"]) == 1 + folder = [x for x in t if x.get("id") == "folder"][0] + assert not folder["leaf"] + assert folder["size"] == 3 + assert stat.S_ISDIR(folder["mode"]) - self.assertEqual(len([x.get('id') for x in t if x.get('id') == 'test_utils.py']), 1) - test_utils = [x for x in t if x.get('id') == 'test_utils.py'][0] - assert test_utils['leaf'] - assert not stat.S_ISDIR(test_utils['mode']) + self.assertEqual(len([x.get("id") for x in t if x.get("id") == "test_utils.py"]), 1) + test_utils = [x for x in t if x.get("id") == "test_utils.py"][0] + assert test_utils["leaf"] + assert not stat.S_ISDIR(test_utils["mode"]) t = get_tree("files/folder", ["long"]) - assert len([x.get('id') for x in t if x.get('id') == '1']) == 1 - one = [x for x in t if x.get('id') == '1'][0] - assert one['leaf'] + assert len([x.get("id") for x in t if x.get("id") == "1"]) == 1 + one = [x for x in t if x.get("id") == "1"][0] + assert one["leaf"] def test_git(self): with app.app_context(): @@ -80,17 +80,17 @@ def test_git(self): class TestTreeGitDisabled(TestCase): def setUp(self): self.root_dir = Path(__file__).parent - configuration.conf.set(PLUGIN_NAME, 'git_init_repo', 'False') - configuration.conf.set(PLUGIN_NAME, 'root_directory', str(self.root_dir)) - configuration.conf.set(PLUGIN_NAME, 'git_enabled', 'False') - os.environ['GIT_AUTHOR_NAME'] = os.environ['GIT_COMMITTER_NAME'] = 'git_author_name' - os.environ['GIT_AUTHOR_EMAIL'] = os.environ['GIT_COMMITTER_EMAIL'] = 'git_author_email' + configuration.conf.set(PLUGIN_NAME, "git_init_repo", "False") + configuration.conf.set(PLUGIN_NAME, "root_directory", str(self.root_dir)) + configuration.conf.set(PLUGIN_NAME, "git_enabled", "False") + os.environ["GIT_AUTHOR_NAME"] = os.environ["GIT_COMMITTER_NAME"] = "git_author_name" + os.environ["GIT_AUTHOR_EMAIL"] = os.environ["GIT_COMMITTER_EMAIL"] = "git_author_email" def test_tree(self): with app.app_context(): t = get_tree() assert len(t) > 0 - assert 'git' not in (x['id'] for x in t) + assert "git" not in (x["id"] for x in t) t = get_tree("tags") assert t == [] t = get_tree("local-branches") @@ -99,6 +99,84 @@ def test_tree(self): assert t == [] t = get_tree("files") print(t) - assert len([x.get('id') for x in t if x.get('id') == 'test_utils.py']) == 1 + assert len([x.get("id") for x in t if x.get("id") == "test_utils.py"]) == 1 t = get_tree("files/folder") - assert len([x.get('id') for x in t if x.get('id') == '1']) == 1 + assert len([x.get("id") for x in t if x.get("id") == "1"]) == 1 + + +class TestStat(TestCase): + def setUp(self): + self.root_dir = Path(__file__).parent + configuration.conf.set(PLUGIN_NAME, "git_init_repo", "False") + configuration.conf.set(PLUGIN_NAME, "root_directory", str(self.root_dir)) + + def test_tree(self): + with app.app_context(): + t = get_stat() + print(t) + assert t is not None + assert t["id"] is None + assert t["exists"] + assert not t["leaf"] + + def test_tags(self): + with app.app_context(): + t = get_stat("tags") + assert t is not None + assert t["id"] == "tags" + assert t["exists"] + assert not t["leaf"] + + def test_local_branches(self): + with app.app_context(): + t = get_stat("local-branches") + assert t is not None + assert t["id"] == "local-branches" + assert t["exists"] + assert not t["leaf"] + + def test_remote_branches(self): + with app.app_context(): + t = get_stat("remote-branches") + assert t is not None + assert t["id"] == "remote-branches" + assert t["exists"] + assert not t["leaf"] + + def test_files(self): + with app.app_context(): + t = get_stat("files") + assert t is not None + assert t["id"] == "files" + assert t["exists"] + assert not t["leaf"] + + t = get_stat("files/test_utils.py") + assert t is not None + assert t["id"] == "files/test_utils.py" + assert t["exists"] + assert t["leaf"] + + t = get_stat("files/not-found") + assert t is not None + assert not t["exists"] + assert t["leaf"] is None + + def test_files_folder(self): + with app.app_context(): + t = get_stat("files/folder") + assert t is not None + assert t["id"] == "files/folder" + assert t["exists"] + assert not t["leaf"] + + t = get_stat("files/folder/1") + assert t is not None + assert t["id"] == "files/folder/1" + assert t["exists"] + assert t["leaf"] + + t = get_stat("files/folder/not-found") + assert t is not None + assert not t["exists"] + assert t["leaf"] is None