From 22881a3112c510e47ce0ec7561520f887087b750 Mon Sep 17 00:00:00 2001
From: Andreas Motl <andreas.motl@panodata.org>
Date: Sun, 19 Jan 2025 00:21:08 +0100
Subject: [PATCH] SFA: Use application loader from `pueblo.sfa`

---
 docs/source/cli.rst      |   6 +-
 responder/util/common.py |   9 ---
 responder/util/python.py | 129 +++------------------------------------
 setup.py                 |   9 ++-
 4 files changed, 20 insertions(+), 133 deletions(-)
 delete mode 100644 responder/util/common.py

diff --git a/docs/source/cli.rst b/docs/source/cli.rst
index 47f3e7e..9298642 100644
--- a/docs/source/cli.rst
+++ b/docs/source/cli.rst
@@ -68,7 +68,11 @@ Launch Remote File
 ------------------
 
 You can also launch a single-file application where its Python file is stored
-on a remote location.
+on a remote location after installing the ``cli-full`` extra.
+
+.. code-block:: shell
+
+    uv pip install 'responder[cli-full]'
 
 Responder supports all filesystem adapters compatible with `fsspec`_, and
 installs the adapters for Azure Blob Storage (az), Google Cloud Storage (gs),
diff --git a/responder/util/common.py b/responder/util/common.py
deleted file mode 100644
index f8389cb..0000000
--- a/responder/util/common.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from urllib.parse import urlparse
-
-
-def is_valid_url(url):
-    try:
-        result = urlparse(url)
-        return all([result.scheme, result.netloc])
-    except ValueError:
-        return False
diff --git a/responder/util/python.py b/responder/util/python.py
index 1953ed4..2a2afd6 100644
--- a/responder/util/python.py
+++ b/responder/util/python.py
@@ -1,32 +1,17 @@
-import importlib
-import importlib.util
 import logging
-import sys
 import typing as t
-import uuid
-from pathlib import Path
-from tempfile import NamedTemporaryFile
-from types import ModuleType
 
-from upath import UPath
+from pueblo.sfa.core import InvalidTarget, SingleFileApplication
 
-from responder.util.common import is_valid_url
+__all__ = [
+    "InvalidTarget",
+    "SingleFileApplication",
+    "load_target",
+]
 
 logger = logging.getLogger(__name__)
 
 
-class InvalidTarget(Exception):
-    """
-    Raised when the target specification format is invalid.
-
-    This exception is raised when the target string does not conform to the expected
-    format of either a module path (e.g., 'acme.app:foo') or a file path
-    (e.g., '/path/to/acme/app.py').
-    """
-
-    pass
-
-
 def load_target(target: str, default_property: str = "api", method: str = "run") -> t.Any:
     """
     Load Python code from a file path or module name.
@@ -54,102 +39,6 @@ def load_target(target: str, default_property: str = "api", method: str = "run")
         >>> api.run()
     """  # noqa: E501
 
-    app_file = None
-    if is_valid_url(target):
-        upath = UPath(target)
-        frag = upath._url.fragment
-        suffix = upath.suffix
-        suffix = suffix.replace(f"#{frag}", "")
-        logger.info(f"Loading remote single-file application, source: {upath}")
-        name = "_".join([upath.parent.stem, upath.stem])
-        app_file = NamedTemporaryFile(prefix=f"{name}_", suffix=suffix, delete=False)
-        target = app_file.name
-        if frag:
-            target = f"{app_file.name}:{frag}"
-        logger.info(f"Writing remote single-file application, target: {target}")
-        app_file.write(upath.read_bytes())
-        app_file.flush()
-
-    # Sanity checks, as suggested by @coderabbitai. Thanks.
-    if not target or (":" in target and len(target.split(":")) != 2):
-        raise InvalidTarget(f"Invalid target format: {target}")
-
-    # Decode launch target location address.
-    # Module: acme.app:foo
-    # Path:   /path/to/acme/app.py:foo
-    target_fragments = target.split(":")
-    if len(target_fragments) > 1:
-        target = target_fragments[0]
-        prop = target_fragments[1]
-    else:
-        prop = default_property
-
-    # Validate property name follows Python identifier rules.
-    if not prop.isidentifier():
-        raise ValueError(f"Invalid property name: {prop}")
-
-    # Import launch target. Treat input location either as a filesystem path
-    # (/path/to/acme/app.py), or as a module address specification (acme.app).
-    path = Path(target)
-    if path.is_file():
-        app = load_file_module(path)
-    else:
-        app = importlib.import_module(target)
-
-    # Invoke launch target.
-    msg_prefix = f"Failed to import target '{target}'"
-    try:
-        api = getattr(app, prop, None)
-        if api is None:
-            raise AttributeError(f"Module has no API instance attribute '{prop}'")
-        if not hasattr(api, method):
-            raise AttributeError(f"API instance '{prop}' has no method '{method}'")
-        return api
-    except ImportError as ex:
-        raise ImportError(f"{msg_prefix}: {ex}") from ex
-    except AttributeError as ex:
-        raise AttributeError(f"{msg_prefix}: {ex}") from ex
-    except Exception as ex:
-        raise RuntimeError(f"{msg_prefix}: Unexpected error: {ex}") from ex
-
-
-def load_file_module(path: Path) -> ModuleType:
-    """
-    Load a Python file as a module using importlib.
-
-    Args:
-        path: Path to the Python file to load
-
-    Returns:
-        The loaded module object
-
-    Raises:
-        ImportError: If the module cannot be loaded
-    """
-
-    # Validate file extension
-    if path.suffix != ".py":
-        raise ValueError(f"File must have .py extension: {path}")
-
-    # Use unique surrogate module name.
-    unique_id = uuid.uuid4().hex
-    name = f"__{path.stem}_{unique_id}__"
-
-    spec = importlib.util.spec_from_file_location(name, path)
-    if spec is None or spec.loader is None:
-        raise ImportError(f"Failed loading module from file: {path}")
-    app = importlib.util.module_from_spec(spec)
-    sys.modules[name] = app
-    try:
-        spec.loader.exec_module(app)
-        return app
-    except (ImportError, SyntaxError) as ex:
-        sys.modules.pop(name, None)
-        raise ImportError(
-            f"Failed to execute module '{app}': {ex.__class__.__name__}: {ex}"
-        ) from ex
-    except Exception as ex:
-        sys.modules.pop(name, None)
-        raise RuntimeError(
-            f"Unexpected error executing module '{app}': {ex.__class__.__name__}: {ex}"
-        ) from ex
+    app = SingleFileApplication.from_spec(spec=target, default_property=default_property)
+    app.load()
+    return app.entrypoint
diff --git a/setup.py b/setup.py
index 055801f..03e3449 100644
--- a/setup.py
+++ b/setup.py
@@ -121,8 +121,11 @@ def run(self):
     extras_require={
         "cli": [
             "docopt-ng",
-            "fsspec[abfs,gcs,github,http,libarchive,s3]",
-            "universal-pathlib",
+            "pueblo[sfa] @ git+https://github.com/pyveci/pueblo@sfa"
+        ],
+        "cli-full": [
+            "responder[cli]",
+            "pueblo[sfa-full] @ git+https://github.com/pyveci/pueblo@sfa"
         ],
         "develop": [
             "poethepoet",
@@ -138,7 +141,7 @@ def run(self):
             "sphinx-design-elements",
             "sphinxext.opengraph",
         ],
-        "full": ["responder[cli,graphql,openapi]"],
+        "full": ["responder[cli-full,graphql,openapi]"],
         "graphql": ["graphene<3", "graphql-server-core>=1.2,<2"],
         "openapi": ["apispec>=1.0.0"],
         "release": ["build", "twine"],