-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow
run
command to accept dist directory
The `run` command now accepts paths to both source and dist directories. Source directories will be built on-the-fly, while dist directories can be executed directly.
- Loading branch information
Showing
7 changed files
with
100 additions
and
60 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,7 @@ description = "Library and toolset for the development of QuestionPy packages" | |
authors = ["innoCampus <[email protected]>"] | ||
license = "MIT" | ||
homepage = "https://questionpy.org" | ||
version = "0.2.5" | ||
version = "0.2.6" | ||
packages = [ | ||
{ include = "questionpy" }, | ||
{ include = "questionpy_sdk" } | ||
|
@@ -28,7 +28,7 @@ python = "^3.11" | |
aiohttp = "^3.9.3" | ||
pydantic = "^2.6.4" | ||
PyYAML = "^6.0.1" | ||
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "763c15bcf3906ebb80d8d0bd9c15e842de4f8b0c" } | ||
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "263750b29fb5ac3b883422141bed13cdb3bd0706" } | ||
jinja2 = "^3.1.3" | ||
aiohttp-jinja2 = "^1.6" | ||
lxml = "~5.1.0" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,14 @@ | ||
# This file is part of the QuestionPy SDK. (https://questionpy.org) | ||
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import zipfile | ||
from pathlib import Path | ||
|
||
import click | ||
from pydantic import ValidationError | ||
|
||
from questionpy_common.constants import DIST_DIR, MANIFEST_FILENAME | ||
from questionpy_common.manifest import Manifest | ||
from questionpy_sdk.package.builder import DirPackageBuilder | ||
from questionpy_sdk.package.errors import PackageBuildError, PackageSourceValidationError | ||
from questionpy_sdk.package.source import PackageSource | ||
|
@@ -19,40 +19,43 @@ | |
) | ||
|
||
|
||
def build_dir_package(source_path: Path) -> None: | ||
def _get_dir_package_location(source_path: Path) -> DirPackageLocation: | ||
try: | ||
return DirPackageLocation(source_path) | ||
except (OSError, ValidationError, ValueError) as exc: | ||
msg = f"Failed to read package manifest:\n{exc}" | ||
raise click.ClickException(msg) from exc | ||
|
||
|
||
def _get_dir_package_location_from_source(pkg_string: str, source_path: Path) -> DirPackageLocation: | ||
# Always rebuild package. | ||
try: | ||
package_source = PackageSource(source_path) | ||
except PackageSourceValidationError as exc: | ||
raise click.ClickException(str(exc)) from exc | ||
|
||
try: | ||
with DirPackageBuilder(package_source) as builder: | ||
builder.write_package() | ||
click.echo(f"Successfully built package '{pkg_string}'.") | ||
except PackageBuildError as exc: | ||
msg = f"Failed to build package: {exc}" | ||
raise click.ClickException(msg) from exc | ||
|
||
return _get_dir_package_location(source_path / DIST_DIR) | ||
|
||
def get_package_location(pkg_string: str) -> PackageLocation: | ||
pkg_path = Path(pkg_string) | ||
|
||
def get_package_location(pkg_string: str, pkg_path: Path) -> PackageLocation: | ||
if pkg_path.is_dir(): | ||
# Always rebuild package. | ||
build_dir_package(pkg_path) | ||
click.echo(f"Successfully built package '{pkg_string}'.") | ||
try: | ||
manifest_path = pkg_path / DIST_DIR / MANIFEST_FILENAME | ||
with manifest_path.open() as manifest_fp: | ||
manifest = Manifest.model_validate_json(manifest_fp.read()) | ||
return DirPackageLocation(pkg_path, manifest) | ||
except (OSError, ValidationError, ValueError) as exc: | ||
msg = f"Failed to read package manifest:\n{exc}" | ||
raise click.ClickException(msg) from exc | ||
# dist dir | ||
if (pkg_path / MANIFEST_FILENAME).is_file(): | ||
return _get_dir_package_location(pkg_path) | ||
# source dir | ||
return _get_dir_package_location_from_source(pkg_string, pkg_path) | ||
|
||
if zipfile.is_zipfile(pkg_path): | ||
return ZipPackageLocation(pkg_path) | ||
|
||
msg = f"'{pkg_string}' doesn't look like a QPy package zip file or directory." | ||
msg = f"'{pkg_string}' doesn't look like a QPy package file, source directory, or dist directory." | ||
raise click.ClickException(msg) | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from pathlib import Path | ||
|
||
import click | ||
|
||
from questionpy_sdk.commands._helper import get_package_location | ||
|
@@ -19,5 +21,6 @@ def run(package: str) -> None: | |
- a dist directory, or | ||
- a source directory (built on-the-fly). | ||
""" # noqa: D301 | ||
web_server = WebServer(get_package_location(package)) | ||
pkg_path = Path(package).resolve() | ||
web_server = WebServer(get_package_location(package, pkg_path)) | ||
web_server.start_server() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from functools import cached_property | ||
from pathlib import Path | ||
|
||
import yaml | ||
|
@@ -28,41 +29,20 @@ def __init__(self, path: Path): | |
PackageSourceValidationError: If the package source could not be validated. | ||
""" | ||
self._path = path | ||
self._config = self._read_yaml_config() | ||
self._validate() | ||
|
||
def _validate(self) -> None: | ||
self._check_required_paths() | ||
|
||
def _check_required_paths(self) -> None: | ||
# check for `python/NAMESPACE/SHORTNAME/__init__.py` | ||
package_init_path = self._path / "python" / self._config.namespace / self._config.short_name / "__init__.py" | ||
try: | ||
package_init_path.stat() | ||
except FileNotFoundError as exc: | ||
msg = f"Expected '{package_init_path}' to exist" | ||
raise PackageSourceValidationError(msg) from exc | ||
package_init_path = self._path / "python" / self.config.namespace / self.config.short_name / "__init__.py" | ||
if not package_init_path.is_file(): | ||
msg = f"Expected '{package_init_path}' to be a file" | ||
msg = f"Expected '{package_init_path}' to exist" | ||
raise PackageSourceValidationError(msg) | ||
|
||
@property | ||
@cached_property | ||
def config(self) -> PackageConfig: | ||
return self._config | ||
|
||
@property | ||
def config_path(self) -> Path: | ||
return self._path / PACKAGE_CONFIG_FILENAME | ||
|
||
@property | ||
def normalized_filename(self) -> str: | ||
return create_normalized_filename(self._config) | ||
|
||
@property | ||
def path(self) -> Path: | ||
return self._path | ||
|
||
def _read_yaml_config(self) -> PackageConfig: | ||
try: | ||
with self.config_path.open() as config_file: | ||
return PackageConfig.model_validate(yaml.safe_load(config_file)) | ||
|
@@ -76,3 +56,15 @@ def _read_yaml_config(self) -> PackageConfig: | |
# TODO: pretty error feedback (https://docs.pydantic.dev/latest/errors/errors/#customize-error-messages) | ||
msg = f"Failed to validate package config '{self.config_path}': {exc}" | ||
raise PackageSourceValidationError(msg) from exc | ||
|
||
@property | ||
def config_path(self) -> Path: | ||
return self._path / PACKAGE_CONFIG_FILENAME | ||
|
||
@property | ||
def normalized_filename(self) -> str: | ||
return create_normalized_filename(self.config) | ||
|
||
@property | ||
def path(self) -> Path: | ||
return self._path |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,17 @@ | |
# The QuestionPy SDK is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import asyncio | ||
import contextlib | ||
import os | ||
import signal | ||
import subprocess | ||
import sys | ||
from collections.abc import Iterator | ||
from asyncio.subprocess import PIPE, Process | ||
from collections.abc import AsyncIterator, Iterable, Iterator | ||
from pathlib import Path | ||
from typing import Any | ||
|
||
import aiohttp | ||
import pytest | ||
from click.testing import CliRunner, Result | ||
|
||
|
@@ -60,13 +62,41 @@ def cwd(isolated_runner: tuple[CliRunner, Path]) -> Path: | |
return isolated_runner[1] | ||
|
||
|
||
@pytest.fixture | ||
async def client_session() -> AsyncIterator[aiohttp.ClientSession]: | ||
async with aiohttp.ClientSession() as session: | ||
yield session | ||
|
||
|
||
# can't test long-running processes with `CliRunner` (https://github.com/pallets/click/issues/2171) | ||
@contextlib.contextmanager | ||
def long_running_cmd(args: list[str]) -> Iterator[subprocess.Popen]: | ||
@contextlib.asynccontextmanager | ||
async def long_running_cmd(args: Iterable[str], timeout: float = 5) -> AsyncIterator[Process]: | ||
try: | ||
popen_args = [sys.executable, "-m", "questionpy_sdk", "--", *args] | ||
proc = subprocess.Popen(popen_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
proc = await asyncio.create_subprocess_exec(*popen_args, stdin=PIPE, stdout=PIPE, stderr=PIPE) | ||
|
||
# ensure tests don't hang indefinitely | ||
async def kill_after_timeout() -> None: | ||
await asyncio.sleep(timeout) | ||
proc.send_signal(signal.SIGINT) | ||
|
||
kill_task = asyncio.create_task(kill_after_timeout()) | ||
yield proc | ||
|
||
finally: | ||
if kill_task: | ||
kill_task.cancel() | ||
proc.send_signal(signal.SIGINT) | ||
proc.wait() | ||
await proc.wait() | ||
|
||
|
||
async def assert_webserver_is_up(session: aiohttp.ClientSession, url: str = "http://localhost:8080/") -> None: | ||
for _ in range(50): # allow 5 sec to come up | ||
try: | ||
async with session.get(url) as response: | ||
assert response.status == 200 | ||
return | ||
except aiohttp.ClientConnectionError: | ||
await asyncio.sleep(0.1) | ||
|
||
pytest.fail("Webserver didn't come up") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters