diff --git a/MANIFEST.in b/MANIFEST.in index 399f77679..223cde175 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,4 @@ include requirements.txt include requirements-test.txt -include hamilton/server/requirements-mini.txt -recursive-include hamilton/server/build * include LICENSE include *.md diff --git a/hamilton/cli/__main__.py b/hamilton/cli/__main__.py index 5071f1a98..2b64ac3b3 100644 --- a/hamilton/cli/__main__.py +++ b/hamilton/cli/__main__.py @@ -290,11 +290,21 @@ def ui( port: int = 8241, base_dir: str = os.path.join(Path.home(), ".hamilton", "db"), no_migration: bool = False, + no_open: bool = False, ): """Runs the Hamilton UI on sqllite in port 8241""" - from hamilton.server import commands + try: + from hamilton_ui import commands + except ImportError: + logger.error( + "hamilton[ui] not installed -- you have to install this to run the UI. " + 'Run `pip install "sf-hamilton[ui]"` to install and get started with the UI!' + ) + raise typer.Exit(code=1) - ctx.invoke(commands.run, port=port, base_dir=base_dir, no_migration=no_migration) + ctx.invoke( + commands.run, port=port, base_dir=base_dir, no_migration=no_migration, no_open=no_open + ) if __name__ == "__main__": diff --git a/hamilton/server b/hamilton/server deleted file mode 120000 index d33ca48c3..000000000 --- a/hamilton/server +++ /dev/null @@ -1 +0,0 @@ -../ui/backend/server \ No newline at end of file diff --git a/hamilton/version.py b/hamilton/version.py index 61660fc43..bea0767dc 100644 --- a/hamilton/version.py +++ b/hamilton/version.py @@ -1 +1 @@ -VERSION = (1, 65, 0, "rc1") +VERSION = (1, 66, 0, "rc1") diff --git a/setup.py b/setup.py index 85d7a0a70..004f3997b 100644 --- a/setup.py +++ b/setup.py @@ -35,14 +35,6 @@ def load_requirements(): return list(requirements) -def load_server_requirements(): - # TODO -- confirm below works/delete this - requirements = {"click", "loguru", "requests", "typer"} - with open("hamilton/server/requirements-mini.txt") as f: - requirements.update(line.strip() for line in f) - return list(requirements) - - setup( name="sf-hamilton", # there's already a hamilton in pypi version=VERSION, @@ -105,13 +97,12 @@ def load_server_requirements(): "diskcache": ["diskcache"], "cli": ["typer"], "sdk": ["sf-hamilton-sdk"], - "ui": load_server_requirements(), + "ui": ["sf-hamilton-ui"], }, entry_points={ "console_scripts": [ "h_experiments = hamilton.plugins.h_experiments.__main__:main", "hamilton = hamilton.cli.__main__:cli", - "hamilton-serve = hamilton.server.__main__:run", "hamilton-admin-build-ui = hamilton.admin:build_ui", "hamilton-admin-build-and-publish = hamilton.admin:build_and_publish", ] diff --git a/ui/README.md b/ui/README.md index 7b8eb39a9..bf38eb9bc 100644 --- a/ui/README.md +++ b/ui/README.md @@ -90,11 +90,19 @@ on local mode. The frontend is a simple React application. There are a few authe but the default is to use local/unauthenticated (open). Please talk to us if you have a need for more custom authentication. -## License +## Development -There are a few directories that are not licensed under the BSD-3 Clear Clause license. These are: -* frontend/src/ee -* backend/server/trackingserver_auth +The structure involves a bit of cleverness to ensure the UI can easily be deployed and served from the CLI. -See the main repository [LICENSE](../LICENSE) for details, else the LICENSE file in the respective directories -mentioned above. +We have a symlink from `backend/hamilton_ui` to `backend/server`, allowing us to work with django's structure +while simultaneously allowing for import as hamilton_ui. (this should probably be changed at some point but not worth it now). + +To deploy, use the `admin.py` script in the UI directory. + +This: + +1. Builds the frontend +2. Copies it into the build/ directory +3. Publishes to the [sf-hamilton-ui](https://pypi.org/project/sf-hamilton-ui/) package on pypi + +Then you'll run it with `hamilton ui` after installing `sf-hamilton[ui]`. diff --git a/hamilton/admin.py b/ui/admin.py similarity index 88% rename from hamilton/admin.py rename to ui/admin.py index 257ac2343..db241de13 100644 --- a/hamilton/admin.py +++ b/ui/admin.py @@ -66,10 +66,10 @@ def _build_ui(): # building the UI cmd = "npm run build --prefix ui/frontend" _command(cmd, capture_output=False) - # wipring the old build if it exists - cmd = "rm -rf hamilton/server/build" + # wiping the old build if it exists + cmd = "rm -rf ui/backend/build" _command(cmd, capture_output=False) - cmd = "cp -R ui/frontend/build hamilton/server/build" + cmd = "cp -R ui/frontend/build ui/backend/server/build" _command(cmd, capture_output=False) @@ -87,9 +87,10 @@ def build_ui(): @click.option("--no-wipe-dist", is_flag=True, help="Wipe the dist/ directory before building") def build_and_publish(prod: bool, no_wipe_dist: bool): git_root = _get_git_root() - with cd(git_root): - logger.info("Building UI -- this may take a bit...") - _build_ui() + install_path = os.path.join(git_root, "ui/backend") + logger.info("Building UI -- this may take a bit...") + # build_ui.callback() # use the underlying function, not click's object + with cd(install_path): logger.info("Built UI!") if not no_wipe_dist: logger.info("Wiping dist/ directory for a clean publish.") diff --git a/ui/backend/MANIFEST.in b/ui/backend/MANIFEST.in new file mode 100644 index 000000000..fd001b5a1 --- /dev/null +++ b/ui/backend/MANIFEST.in @@ -0,0 +1,4 @@ +include hamilton_ui/requirements-mini.txt +recursive-include hamilton_ui/build * +include ../../LICENSE +include *.md diff --git a/ui/backend/hamilton_ui b/ui/backend/hamilton_ui new file mode 120000 index 000000000..13cd1fa7f --- /dev/null +++ b/ui/backend/hamilton_ui @@ -0,0 +1 @@ +server \ No newline at end of file diff --git a/ui/backend/server/commands.py b/ui/backend/server/commands.py index a523ef1be..4fd2efff2 100644 --- a/ui/backend/server/commands.py +++ b/ui/backend/server/commands.py @@ -1,11 +1,14 @@ import os import sys +import threading +import time +import webbrowser from contextlib import contextmanager +import hamilton_ui +import requests from django.core.management import execute_from_command_line -import hamilton - @contextmanager def extend_sys_path(path): @@ -40,13 +43,36 @@ def set_env_variables(vars: dict): os.environ[key] = original_values[key] -def run(port: int, base_dir: str, no_migration: bool): +def _open_when_ready(check_url: str, open_url: str): + while True: + try: + response = requests.get(check_url) + if response.status_code == 200: + webbrowser.open(open_url) + return + else: + pass + except requests.exceptions.RequestException: + pass + time.sleep(0.1) + + +def run(port: int, base_dir: str, no_migration: bool, no_open: bool): env = { - "DJANGO_SETTINGS_MODULE": "hamilton.server.server.settings_mini", "HAMILTON_BASE_DIR": base_dir, } + if not no_open: + thread = threading.Thread( + target=_open_when_ready, + kwargs={ + "open_url": (open_url := f"http://localhost:{port}"), + "check_url": f"{open_url}/api/v0/health", + }, + daemon=True, + ) + thread.start() with set_env_variables(env): - with extend_sys_path(os.path.join(*hamilton.__path__, "server")): + with extend_sys_path(hamilton_ui.__path__[0]): if not no_migration: execute_from_command_line( ["manage.py", "migrate", "--settings=server.settings_mini"] diff --git a/ui/backend/server/trackingserver_base/api.py b/ui/backend/server/trackingserver_base/api.py index 0eb54c59b..af61e0c6f 100644 --- a/ui/backend/server/trackingserver_base/api.py +++ b/ui/backend/server/trackingserver_base/api.py @@ -67,3 +67,12 @@ async def get_node_metadata_types( ) -> AllNodeMetadataTypes: error = HttpError(status_code=400, message="This only exists to populate the openAPI schema.") raise error + + +@router.get( + "/v1/health", + tags=["health"], + response=bool, +) +def health_check(request): + return True diff --git a/ui/backend/setup.py b/ui/backend/setup.py new file mode 100644 index 000000000..8819bb4df --- /dev/null +++ b/ui/backend/setup.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""The setup script.""" + +from setuptools import find_packages, setup + +REQUIREMENTS_FILES = ["requirements.txt"] + + +def load_requirements(): + # TODO -- confirm below works/delete this + requirements = {"click", "loguru", "requests", "typer"} + with open("hamilton_ui/requirements-mini.txt") as f: + requirements.update(line.strip() for line in f) + return list(requirements) + + +setup( + name="sf-hamilton-ui", # there's already a hamilton in pypi + version="0.0.3", + description="Hamilton, the micro-framework for creating dataframes.", + long_description="""Hamilton tracking server, see [the docs for more](https://github.com/dagworks-inc/hamilton/tree/main/ui/)""", + long_description_content_type="text/markdown", + author="Stefan Krawczyk, Elijah ben Izzy", + author_email="stefan@dagworks.io,elijah@dagworks.io", + url="https://github.com/dagworks-inc/hamilton", + packages=find_packages(exclude=["tests"], include=["hamilton_ui", "hamilton_ui.*"]), + include_package_data=True, + install_requires=load_requirements(), + zip_safe=False, + keywords="hamilton", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + # Note that this feature requires pep8 >= v9 and a version of setup tools greater than the + # default version installed with virtualenv. Make sure to update your tools! + python_requires=">=3.6, <4", + # adding this to slim the package down, since these dependencies are only used in certain contexts. + # Relevant project URLs + project_urls={ # Optional + "Bug Reports": "https://github.com/dagworks-inc/hamilton/issues", + "Source": "https://github.com/dagworks-inc/hamilton", + }, +)