Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions shapeflow/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class _VideoAnalyzerDispatcher(Dispatcher):

:func:`shapeflow.core.backend.BaseAnalyzer.cancel`
"""
get_config = Endpoint(Callable[[], dict], stream_json)
get_config = Endpoint(Callable[[], dict], stream_json) # todo: GET/POST on single endpoint
"""Return the analyzer's configuration

:func:`shapeflow.core.backend.BaseAnalyzer.get_config`
Expand Down Expand Up @@ -243,7 +243,7 @@ class _VideoAnalyzerManagerDispatcher(Dispatcher):
:func:`shapeflow.main._VideoAnalyzerManager.load_state`
"""

stream = Endpoint(Callable[[str, str], BaseStreamer])
stream = Endpoint(Callable[[str, str], BaseStreamer]) # todo: GET/POST on single endpoint
"""Open a new stream for a given analyzer ID and endpoint

:func:`shapeflow.main._VideoAnalyzerManager.stream`
Expand Down Expand Up @@ -371,7 +371,7 @@ class ApiDispatcher(Dispatcher):

:func:`shapeflow.main._Main.normalize_config`
"""
get_settings = Endpoint(Callable[[], dict])
get_settings = Endpoint(Callable[[], dict]) # todo: GET/POST on single endpoint
"""Get the application settings

:func:`shapeflow.main._Main.get_settings`
Expand All @@ -381,7 +381,7 @@ class ApiDispatcher(Dispatcher):

:func:`shapeflow.main._Main.set_settings`
"""
events = Endpoint(Callable[[], EventStreamer], stream_json)
events = Endpoint(Callable[[], EventStreamer], stream_json) # todo: GET/POST on single endpoint
"""Open an event stream

:func:`shapeflow.main._Main.events`
Expand All @@ -391,7 +391,7 @@ class ApiDispatcher(Dispatcher):

:func:`shapeflow.main._Main.stop_events`
"""
log = Endpoint(Callable[[], PlainFileStreamer], stream_plain)
log = Endpoint(Callable[[], PlainFileStreamer], stream_plain) # todo: GET/POST on single endpoint
"""Open a log stream

:func:`shapeflow.main._Main.log`
Expand All @@ -401,6 +401,16 @@ class ApiDispatcher(Dispatcher):

:func:`shapeflow.main._Main.stop_log`
"""
command = Endpoint(Callable[[str, dict], None])
"""Execute a ``shapeflow.cli.Command``

:func:`shapeflow.main._Main.command`
"""
resolve_prompt = Endpoint(Callable[[str, Any], None])
"""Respond to a prompt

:func:`shapeflow.main._Main.prompt`
"""
unload = Endpoint(Callable[[], bool])
"""Unload the application. In order to support page reloading, the backend
will wait for some time and quit if no further requests come in.
Expand Down
97 changes: 83 additions & 14 deletions shapeflow/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,77 @@
from pathlib import Path
import argparse
import shutil
from enum import Enum
from threading import Queue
from functools import lru_cache
from typing import List, Callable, Optional, Tuple
from typing import List, Optional, Tuple
from urllib.request import urlretrieve
from zipfile import ZipFile

from distutils.util import strtobool
import git
import requests
import shortuuid

from shapeflow import __version__, get_logger, settings
from shapeflow.core.streaming import EventStreamer, EventCategory
from shapeflow.util import before_version, after_version, suppress_stdout


log = get_logger(__name__)

# type aliases

OptArgs = Optional[List[str]]
Parsing = Callable[[OptArgs], None]


class CliError(Exception):
pass


class PromptType(Enum):
BOOLEAN = 'boolean'


class Prompt(abc.ABC):
def yn(self, prompt: str) -> bool:
raise NotImplementedError


class ConsolePrompt(Prompt):
def yn(self, prompt: str) -> bool:
return strtobool(input(f"{prompt} (y/n) "))


class EventResponsePrompt(Prompt):
_eventstreamer: EventStreamer
_id: str
_queue: Queue

def __init__(self, eventstreamer: EventStreamer):
self._eventstreamer = eventstreamer

def _new_id(self) -> str:
id = shortuuid.uuid()
self._id = id
return id

def yn(self, prompt: str) -> bool:
self._queue = Queue()

id = self._new_id()
self._eventstreamer.event(EventCategory.PROMPT, id, data={
"prompt": prompt, "type": PromptType.BOOLEAN,
})

response = self._queue.get()
assert isinstance(response, bool)
return response

def resolve(self, id: str, data: Any) -> None:
if self._id == id:
self._queue.put(data)


class IterCommand(abc.ABCMeta):
"""Command iterator metaclass.

Expand All @@ -47,7 +94,9 @@ class IterCommand(abc.ABCMeta):
"""
__command__: str
"""Command name. This is how the command is addressed from the commandline.
""" # todo: nope, doesn't work'
"""

parser: argparse.ArgumentParser

def __str__(cls):
try:
Expand Down Expand Up @@ -101,7 +150,14 @@ class Command(abc.ABC, metaclass=IterCommand):
args: argparse.Namespace
sub_args: List[str]

def __init__(self, args: OptArgs = None):
_prompt: Prompt

def __init__(self, args: OptArgs = None, prompt: Prompt = None):
if prompt is None:
self._prompt = ConsolePrompt()
else:
self._prompt = prompt

if args is None:
# gather commandline arguments
args = sys.argv[1:]
Expand Down Expand Up @@ -148,6 +204,10 @@ def _fix_call(cls, text: str) -> str:
else:
return text

@property
def prompt(self):
return self._prompt


class Sf(Command):
"""Commandline entry point.
Expand Down Expand Up @@ -215,6 +275,7 @@ class Serve(Command):

__command__ = 'serve'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)

Expand Down Expand Up @@ -273,6 +334,7 @@ class Dump(Command):

__command__ = 'dump'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand All @@ -288,7 +350,7 @@ class Dump(Command):
)

def command(self):
from shapeflow.config import schemas
from shapeflow.main import schemas

if not self.args.dir.is_dir():
log.warning(f"making directory '{self.args.dir}'")
Expand All @@ -311,6 +373,8 @@ class GitMixin(abc.ABC):
_repo = None
_latest = None

prompt: Prompt

@property
def repo(self) -> git.Repo:
if self._repo is None:
Expand Down Expand Up @@ -404,10 +468,10 @@ def _prompt_discard_changes(self) -> bool:
[item.a_path for item in self.repo.index.diff(None)] \
+ self.repo.untracked_files
)
return bool(strtobool(input(
return self.prompt.yn(
f'Local changes to\n\n {changed} \n\n'
f'will be overwritten. Continue? (y/n) '
)))
f'will be overwritten. Continue?'
)


class Update(Command, GitMixin):
Expand All @@ -416,6 +480,7 @@ class Update(Command, GitMixin):

__command__ = 'update'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand Down Expand Up @@ -447,12 +512,13 @@ def _update(self) -> None:


class Checkout(Command, GitMixin):
"""Check out a specific version of the application. Please not you will
"""Check out a specific version of the application. Please note you will
not have access to this command if you check out a version before 0.4.4
"""

__command__ = 'checkout'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand All @@ -477,11 +543,11 @@ def command(self) -> None:
self._get_compiled_ui()

def _checkout_anyway(self) -> bool:
return bool(strtobool(input(
return self.prompt.yn(
f'After checking out "{self.args.ref}" you won\'t be able to use '
f'the "update" or "checkout" commands (they were added later, '
f'in v0.4.4) Continue? (y/n) '
)))
f'in v0.4.4) Continue?'
)


class GetCompiledUi(Command, GitMixin):
Expand All @@ -490,6 +556,7 @@ class GetCompiledUi(Command, GitMixin):

__command__ = 'get-compiled-ui'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand All @@ -513,7 +580,7 @@ def command(self) -> None:
self._get_compiled_ui()

def _prompt_replace_ui(self) -> bool:
return bool(strtobool(input('Replace the current UI? (y/n) ')))
return self.prompt.yn('Replace the current UI?')


class SetupCairo(Command):
Expand All @@ -522,6 +589,7 @@ class SetupCairo(Command):

__command__ = 'setup-cairo'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand Down Expand Up @@ -619,6 +687,7 @@ class Declutter(Command):

__command__ = 'declutter'
parser = argparse.ArgumentParser(
prog=__command__,
description=__doc__
)
parser.add_argument(
Expand Down
26 changes: 4 additions & 22 deletions shapeflow/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Optional, Tuple, Dict, Any, Type, Union
from typing import Optional, Tuple, Dict, Any
import json

from pydantic import Field, validator

from shapeflow import __version__, settings
from shapeflow import __version__

from shapeflow.core.config import extend, ConfigType, \
log, VERSION, CLASS, untag, BaseConfig
from shapeflow.core.backend import BaseAnalyzerConfig, \
FeatureType, FeatureConfig, AnalyzerState, QueueState
FeatureType, FeatureConfig
from shapeflow.core import EnforcedStr
from shapeflow.core.interface import FilterType, TransformType, TransformConfig, \
FilterConfig, HandlerConfig
Expand Down Expand Up @@ -265,27 +265,9 @@ def _validate_parameters(cls, value, values):

return tuple(parameters)

_validate_fis = validator('frame_interval_setting')(BaseConfig._resolve_enforcedstr)
_validate_fis = validator('frame_interval_setting', allow_reuse=True)(BaseConfig._resolve_enforcedstr)


def schemas() -> Dict[str, dict]:
"""Get the JSON schemas of

* :class:`shapeflow.video.VideoAnalyzerConfig`

* :class:`shapeflow.Settings`

* :class:`shapeflow.core.backend.AnalyzerState`

* :class:`shapeflow.core.backend.QueueState`
"""
return {
'config': VideoAnalyzerConfig.schema(),
'settings': settings.schema(),
'analyzer_state': dict(AnalyzerState.__members__),
'queue_state': dict(QueueState.__members__),
}

def loads(config: str) -> BaseConfig:
"""Load a configuration object from a JSON string.

Expand Down
Loading