Commit e06683de authored by Tanguy Le Carrour's avatar Tanguy Le Carrour
Browse files

Add a CLI.

parent 221d36f5
# List of things to do
- [ ] Figure out if `pydub` should be used in the model or if it's an implementation
detail!? … because it does I/O to read the track!
```
track = track_factory.from_path("track.mp3")
class PydubTrack(Track):
@classmethod
def create(cls, a_path: str) -> Track:
try:
return cls(AudioSegment.from_file(a_path))
except:
pass
def __init__(self, a_pydub_object):
pass
def export(self, path):
self.__segment.export(path)
class PydubTracks(Tracks):
def __getitem__(self, a_path: str) -> Track:
return PydubTrack.create(a_path)
def __setitem__(self, a_path: str, a_track: Track) -> None:
# Here we break LSP
if not instanceof(a_track, PydubTrack):
raise NotImplementedError("Repo only handles PydubTrack objects!")
a_track.export(a_path)
class ExtractSubTracks(UseCase):
def __init__(self, some_tracks: Tracks):
self.__tracks = some_tracks
def execute(self, request):
track = self.__tracks[request.path]
intervals = [Interval(start, end) for (start, end) in request.intervals]
for idx, interval in intervals.enumerate():
sub_track = track.extract(interval)
path = Path(request.path).add_suffix(f"-{idx}")
self.__tracks[path] = sub_track
```
- [ ] make presenter work with `echo` and `exit`: `Presenter(click.echo, sys.exit)`
this allows to stream the result and to simplify the entry point
......@@ -14,6 +14,29 @@ optional = false
python-versions = "*"
version = "0.1.0"
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.2.0"
[package.extras]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
[[package]]
category = "dev"
description = "The uncompromising code formatter."
......@@ -53,7 +76,7 @@ python-versions = "*"
version = "3.0.4"
[[package]]
category = "dev"
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
......@@ -71,6 +94,15 @@ version = "0.5.1"
[package.dependencies]
args = "*"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.4"
[[package]]
category = "dev"
description = "Code coverage measurement for Python"
......@@ -82,6 +114,17 @@ version = "5.3"
[package.extras]
toml = ["toml"]
[[package]]
category = "dev"
description = "Python's missing debug print command and other development tools."
name = "devtools"
optional = false
python-versions = ">=3.6"
version = "0.6.1"
[package.extras]
pygments = ["Pygments (>=2.2.0)"]
[[package]]
category = "dev"
description = "A parser for Python dependency files"
......@@ -106,6 +149,14 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.10"
[[package]]
category = "dev"
description = "iniconfig: brain-dead simple config-ini parsing"
name = "iniconfig"
optional = false
python-versions = "*"
version = "1.1.1"
[[package]]
category = "dev"
description = "Pythonic task execution"
......@@ -183,6 +234,25 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.8.0"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.9.0"
[[package]]
category = "main"
description = "Manipulate audio with an simple and easy high level interface"
......@@ -199,6 +269,28 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "6.1.2"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.8.2"
toml = "*"
[package.extras]
checkqa_mypy = ["mypy (0.780)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "YAML parser and emitter for Python"
......@@ -314,7 +406,7 @@ secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "cer
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[metadata]
content-hash = "448e5d4c2054a2f904e6e6bf40702e090d163a3008202070f9e25e5027aacd16"
content-hash = "4a2a1cde345aedd5d8b42e8de4c286417a594f24edc1b17c48c41a6be8a2e6c2"
lock-version = "1.0"
python-versions = "^3.8"
......@@ -326,8 +418,15 @@ appdirs = [
args = [
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"},
{file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"},
]
black = [
{file = "black-20.8b1-py3-none-any.whl", hash = "sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b"},
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
certifi = [
......@@ -345,6 +444,9 @@ click = [
clint = [
{file = "clint-0.5.1.tar.gz", hash = "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
]
coverage = [
{file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"},
{file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"},
......@@ -381,6 +483,10 @@ coverage = [
{file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"},
{file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"},
]
devtools = [
{file = "devtools-0.6.1-py3-none-any.whl", hash = "sha256:7334183972a8d04e81d08b7f62126abca9b6f4de51d825c5fdcb9c88f252601a"},
{file = "devtools-0.6.1.tar.gz", hash = "sha256:a054307594d35d28fae8df7629967363e851ae0ac7b2152640a8a401c39d42d7"},
]
dparse = [
{file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"},
{file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"},
......@@ -389,6 +495,10 @@ idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
invoke = [
{file = "invoke-1.4.1-py2-none-any.whl", hash = "sha256:93e12876d88130c8e0d7fd6618dd5387d6b36da55ad541481dfa5e001656f134"},
{file = "invoke-1.4.1-py3-none-any.whl", hash = "sha256:87b3ef9d72a1667e104f89b159eaf8a514dbf2f3576885b2bbdefe74c3fb2132"},
......@@ -429,6 +539,14 @@ pathspec = [
{file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
{file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pydub = [
{file = "pydub-0.24.1-py2.py3-none-any.whl", hash = "sha256:25fdfbbfd4c69363006a27c7bd2346c4b886a0dd3da264c14d858b71a9593284"},
{file = "pydub-0.24.1.tar.gz", hash = "sha256:630c68bfff9bb27cbc5e1f02923f717c3bc5f4d73fd685fda08b6ce90f76dc69"},
......@@ -437,6 +555,10 @@ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
{file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
]
pyyaml = [
{file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
{file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
......
......@@ -5,9 +5,13 @@ description = "A tool to split audio files."
authors = ["Tanguy Le Carrour"]
license = "GPL-3.0-or-later"
[tool.poetry.scripts]
splito = "splito.entry_points.cli:app"
[tool.poetry.dependencies]
python = "^3.8"
pydub = "^0.24.1"
click = "^7.1.2"
[tool.poetry.dev-dependencies]
mypy = "^0.790"
......@@ -16,6 +20,8 @@ robber = "^1.1.5"
invoke = "^1.4.1"
black = "^20.8b1"
safety = "^1.9.0"
pytest = "^6.1.2"
devtools = "^0.6.1"
[build-system]
requires = ["poetry>=0.12"]
......
from typing import Any
from click.testing import CliRunner, Result
def invoke(*args: Any, **kwargs: Any) -> Result:
return CliRunner().invoke(*args, **kwargs)
import click
from splito.infrastructure.pydub.tracks import PydubTracks
def app() -> click.Group:
from splito.infrastructure.click import cli
return cli(obj={"tracks": PydubTracks()}) # type: ignore
import sys
from typing import Tuple
import click
from splito.interface.cli import (
ExtractSubTracksController,
ExtractSubTracksPresenter,
)
from splito.use_cases.extract_sub_tracks import ExtractSubTracksInteractor
@click.group()
@click.pass_context
def cli(ctx: click.Context) -> None:
pass
@cli.command()
@click.pass_context
@click.argument("track")
@click.argument("intervals", nargs=-1)
def extract(ctx: click.Context, track: str, intervals: Tuple[str]) -> None:
controller = ExtractSubTracksController(track, list(intervals))
presenter = ExtractSubTracksPresenter()
interactor = ExtractSubTracksInteractor(presenter, ctx.obj["tracks"])
controller.call(interactor)
click.echo(presenter.to_string())
sys.exit(presenter.exit_code())
from robber import expect # type: ignore
from splito.infrastructure.click import extract
from splito.conftest import invoke
class TestExtractSubTracks:
def test_ok(self, tracks): # type: ignore
result = invoke(extract, [], obj={"tracks": tracks})
expect(result.exit_code).to.eq(0)
from typing import List, Tuple
from splito.use_cases.extract_sub_tracks import (
ExtractSubTracksRequest,
ExtractSubTracksInteractor,
ExtractSubTracksPresenter as __ExtractSubTracksPresenter,
)
class ExtractSubTracksController:
def __init__(self, a_path: str, some_interval_paires: List[str]) -> None:
self.__request = ExtractSubTracksRequest(
a_path, self.__extract_intervals(some_interval_paires)
)
def __extract_intervals(
self, some_interval_paires: List[str]
) -> List[Tuple[str, str]]:
intervals: List[Tuple[str, str]] = []
for paire in some_interval_paires:
interval = paire.split("-", 1)
if len(interval) == 2:
intervals.append((interval[0], interval[1]))
else:
intervals.append((interval[0], "0"))
return intervals
def call(self, interactor: ExtractSubTracksInteractor) -> None:
interactor.execute(self.__request)
class ExtractSubTracksPresenter(__ExtractSubTracksPresenter):
def __init__(self) -> None:
self.__exit_code = 1
self.__sub_tracks: List[str] = []
self.__error = ""
def not_an_audio_track(self, a_path: str) -> None:
self.__exit_code = 1
self.__error = f"Not an audio track: {a_path}."
def wrong_interval(self, a_start: str, an_end: str) -> None:
self.__error = f"[WARN] Not a proper interval: {a_start}-{an_end}."
def sub_track_extracted(self, a_path: str) -> None:
self.__exit_code = 0
self.__sub_tracks.append(a_path)
def exit_code(self) -> int:
return self.__exit_code
def to_string(self) -> str:
result = ""
if self.__error:
result += f"[ERR] {self.__error}\n"
if self.__sub_tracks:
result += "Sub tracks extracted:\n"
for sub_track in self.__sub_tracks:
result += f" - {sub_track}\n"
else:
result += "No track extracted!"
return result
import mock
from mamba import describe, it, before # type: ignore
from robber import expect # type: ignore
from splito.use_cases.extract_sub_tracks import (
ExtractSubTracksInteractor,
ExtractSubTracksRequest,
)
from splito.interface.cli import (
ExtractSubTracksController,
ExtractSubTracksPresenter,
)
with describe(ExtractSubTracksController):
with before.each as self:
self.interactor = mock.create_autospec(spec=ExtractSubTracksInteractor)
with describe(ExtractSubTracksController.call):
with it("handles good parameters"):
controller = ExtractSubTracksController("a_path", ["1-2"])
controller.call(self.interactor)
expect(self.interactor.execute).to.have.been.called_with(
ExtractSubTracksRequest("a_path", [("1", "2")])
)
with it("handles bad parameters"):
controller = ExtractSubTracksController("a_path", ["1~2.3"])
controller.call(self.interactor)
expect(self.interactor.execute).to.have.been.called(
ExtractSubTracksRequest("a_path", [("1~2.3", "0")])
)
with describe(ExtractSubTracksPresenter):
with it("handles extracting sub-tracks"):
presenter = ExtractSubTracksPresenter()
presenter.sub_track_extracted("a_path/a_file-1.mp3")
expect(presenter.exit_code()).to.eq(0)
expect(presenter.to_string()).to.contain("a_path/a_file-1.mp3")
with it("handles wrong track"):
presenter = ExtractSubTracksPresenter()
presenter.not_an_audio_track("a_path/a_file-1.mp3")
expect(presenter.exit_code()).to.eq(1)
expect(presenter.to_string()).to.contain("a_path/a_file-1.mp3")
with it("handles wrong intervals"):
presenter = ExtractSubTracksPresenter()
presenter.wrong_interval("1", "0")
expect(presenter.to_string()).to.contain("WARN")
......@@ -28,6 +28,7 @@ def unit(c):
title(unit.__doc__)
c.run("mamba --format documentation splito/domain", pty=True)
c.run("mamba --format documentation splito/use_cases", pty=True)
c.run("mamba --format documentation splito/interface", pty=True)
c.run("mamba --format documentation splito/infrastructure", pty=True)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment