initial commit
This commit is contained in:
parent
a0bd670b9e
commit
0dd3f40a6c
0
croaker/__init__.py
Normal file
0
croaker/__init__.py
Normal file
207
croaker/cli.py
Normal file
207
croaker/cli.py
Normal file
|
@ -0,0 +1,207 @@
|
|||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional
|
||||
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from typing_extensions import Annotated
|
||||
|
||||
import croaker.path
|
||||
from croaker import client, controller, server
|
||||
from croaker.exceptions import ConfigurationError
|
||||
from croaker.playlist import Playlist
|
||||
|
||||
SETUP_HELP = """
|
||||
# Root directory for croaker configuration and logs. See also croaker --root.
|
||||
CROAKER_ROOT=~/.dnd/croaker
|
||||
|
||||
## COMMAND AND CONTROL WEBSERVER
|
||||
|
||||
# Please make sure you set SECRET_KEY in your environment if you are running
|
||||
# the command and control webserver. Clients do not need this.
|
||||
SECRET_KEY=
|
||||
|
||||
# Where the record the webserver daemon's PID
|
||||
PIDFILE=~/.dnd/croaker/croaker.pid
|
||||
|
||||
# Web interface configuration
|
||||
HOST=127.0.0.1
|
||||
PORT=8003
|
||||
|
||||
## CONTROLLER CLIENT
|
||||
|
||||
# The host and port to use when connecting to the websever.
|
||||
CONTROLLER_HOST=127.0.0.1
|
||||
CONTROLLER_PORT=8003
|
||||
|
||||
## MEDIA
|
||||
|
||||
# where to store playlist sources
|
||||
PLAYLIST_ROOT=~/.dnd/croaker/playlists
|
||||
|
||||
# where to cache transcoded media files
|
||||
CACHE_ROOT=~/.dnd/croaker/cache
|
||||
|
||||
# the kinds of files to add to playlists
|
||||
MEDIA_GLOB=*.mp3,*.flac,*.m4a
|
||||
|
||||
# If defined, transcode media before streaming it, and cache it to disk. The
|
||||
# strings INFILE and OUTFILE will be replaced with the media source file and
|
||||
# the cached output location, respectively.
|
||||
TRANSCODER=/usr/bin/ffmpeg -i INFILE '-hide_banner -loglevel error -codec:v copy -codec:a libmp3lame -q:a 2' OUTFILE
|
||||
|
||||
## LIQUIDSOAP AND ICECAST
|
||||
|
||||
# The liquidsoap executable
|
||||
LIQUIDSOAP=/usr/bin/liquidsoap
|
||||
|
||||
# Icecast2 configuration for Liquidsoap
|
||||
ICECAST_PASSWORD=
|
||||
ICECAST_MOUNT=
|
||||
ICECAST_HOST=
|
||||
ICECAST_PORT=
|
||||
ICECAST_URL=
|
||||
"""
|
||||
|
||||
|
||||
app = typer.Typer()
|
||||
app_state = {}
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(
|
||||
context: typer.Context,
|
||||
root: Optional[Path] = typer.Option(
|
||||
Path("~/.dnd/croaker"),
|
||||
help="Path to the Croaker environment",
|
||||
),
|
||||
host: Optional[str] = typer.Option(
|
||||
None,
|
||||
help="bind address",
|
||||
),
|
||||
port: Optional[int] = typer.Option(
|
||||
None,
|
||||
help="bind port",
|
||||
),
|
||||
debug: Optional[bool] = typer.Option(None, help="Enable debugging output"),
|
||||
):
|
||||
load_dotenv(root.expanduser() / Path("defaults"))
|
||||
load_dotenv(stream=io.StringIO(SETUP_HELP))
|
||||
if host:
|
||||
os.environ["HOST"] = host
|
||||
if port:
|
||||
os.environ["PORT"] = port
|
||||
if debug is not None:
|
||||
if debug:
|
||||
os.environ["DEBUG"] = 1
|
||||
else:
|
||||
del os.environ["DEBUG"]
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
)
|
||||
|
||||
try:
|
||||
croaker.path.media_root()
|
||||
croaker.path.cache_root()
|
||||
except ConfigurationError as e:
|
||||
sys.stderr.write(f"{e}\n\n{SETUP_HELP}")
|
||||
sys.exit(1)
|
||||
|
||||
app_state["client"] = client.Client(
|
||||
host=os.environ["CONTROLLER_HOST"],
|
||||
port=os.environ["CONTROLLER_PORT"],
|
||||
)
|
||||
|
||||
if not context.invoked_subcommand:
|
||||
return play(context)
|
||||
|
||||
|
||||
@app.command()
|
||||
def setup(context: typer.Context):
|
||||
"""
|
||||
(Re)Initialize Croaker.
|
||||
"""
|
||||
sys.stderr.write("Interactive setup is not yet available. Sorry!\n")
|
||||
print(dedent(SETUP_HELP))
|
||||
|
||||
|
||||
@app.command()
|
||||
def start(
|
||||
context: typer.Context,
|
||||
daemonize: bool = typer.Option(True, help="Daemonize the webserver."),
|
||||
):
|
||||
"""
|
||||
Start the Croaker command and control webserver.
|
||||
"""
|
||||
controller.start()
|
||||
if daemonize:
|
||||
server.daemonize()
|
||||
else:
|
||||
server.start()
|
||||
|
||||
|
||||
@app.command()
|
||||
def stop():
|
||||
"""
|
||||
Terminate the webserver process and liquidsoap.
|
||||
"""
|
||||
controller.stop()
|
||||
server.stop()
|
||||
|
||||
|
||||
@app.command()
|
||||
def play(
|
||||
playlist: str = typer.Argument(
|
||||
...,
|
||||
help="Playlist name",
|
||||
)
|
||||
):
|
||||
"""
|
||||
Begin playing tracks from the directory $PLAYLIST_ROOT/[NAME].
|
||||
"""
|
||||
res = app_state["client"].play(playlist)
|
||||
if res.status_code == 200:
|
||||
print("OK")
|
||||
|
||||
|
||||
@app.command()
|
||||
def skip():
|
||||
"""
|
||||
Play the next track on the current playlist.
|
||||
"""
|
||||
res = app_state["client"].skip()
|
||||
if res.status_code == 200:
|
||||
print("OK")
|
||||
|
||||
|
||||
@app.command()
|
||||
def add(
|
||||
playlist: str = typer.Argument(
|
||||
...,
|
||||
help="Playlist name",
|
||||
),
|
||||
theme: Optional[bool] = typer.Option(False, help="Make the first track the theme song."),
|
||||
tracks: Annotated[Optional[List[Path]], typer.Argument()] = None,
|
||||
):
|
||||
"""
|
||||
Recursively add one or more paths to the specified playlist. Tracks can be
|
||||
any combination of individual audio files and directories containing audio
|
||||
files; anything not already on the playlist will be added to it.
|
||||
|
||||
If --theme is specified, the first track will be designated the playlist
|
||||
"theme." Theme songs get played first whenever the playlist is loaded,
|
||||
after which the playlist order is randomized.
|
||||
"""
|
||||
pl = Playlist(name=playlist)
|
||||
pl.add(tracks, make_theme=theme)
|
||||
print(pl)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.main()
|
43
croaker/client.py
Normal file
43
croaker/client.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
|
||||
import bottle
|
||||
import requests
|
||||
|
||||
# needs to be imported to attach routes to the default app
|
||||
from croaker import routes
|
||||
|
||||
assert routes
|
||||
|
||||
|
||||
@dataclass
|
||||
class Client:
|
||||
host: str
|
||||
port: int
|
||||
|
||||
@cached_property
|
||||
def _session(self):
|
||||
return requests.Session()
|
||||
|
||||
@property
|
||||
def _routes(self):
|
||||
return [r.callback.__name__ for r in bottle.default_app().routes]
|
||||
|
||||
def get(self, uri: str, *args, **params):
|
||||
url = f"http://{self.host}:{self.port}/{uri}"
|
||||
if args:
|
||||
url += "/" + "/".join(args)
|
||||
res = self._session.get(url, params=params)
|
||||
logging.debug(f"{url = }, {res = }")
|
||||
return res
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in self._routes:
|
||||
|
||||
def dispatch(*args, **kwargs):
|
||||
logging.debug(f"calling attr, {args = }, {kwargs = }")
|
||||
return self.get(attr, *args, **kwargs)
|
||||
|
||||
return dispatch
|
||||
return self.__getattribute__(attr)
|
141
croaker/controller.py
Normal file
141
croaker/controller.py
Normal file
|
@ -0,0 +1,141 @@
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from subprocess import Popen
|
||||
from time import sleep
|
||||
|
||||
from Exscript.protocols import Telnet
|
||||
|
||||
from croaker import path
|
||||
from croaker.pidfile import pidfile
|
||||
from croaker.playlist import Playlist, load_playlist
|
||||
|
||||
NOW_PLAYING = None
|
||||
|
||||
LIQUIDSOAP_CONFIG = """
|
||||
set("server.telnet",true)
|
||||
set("request.grace_time", 1.0)
|
||||
set("init.daemon.pidfile.path", "{pidfile.path}")
|
||||
set("decoder.ffmpeg.codecs.alac", ["alac"])
|
||||
|
||||
|
||||
# deeebuggin
|
||||
set("log.file.path","{debug_log}")
|
||||
|
||||
# set up the stream
|
||||
stream = crossfade(normalize(playlist.safe(
|
||||
id='stream',
|
||||
reload_mode='watch',
|
||||
mode='normal',
|
||||
'{playlist_root}/now_playing',
|
||||
)))
|
||||
|
||||
# if source files don't contain metadata tags, use the filename
|
||||
def apply_metadata(m) =
|
||||
title = m["title"]
|
||||
print("Now Playing: #{{m['filename']}}")
|
||||
if (title == "") then
|
||||
[("title", "#{{path.remove_extension(path.basename(m['filename']))}}")]
|
||||
else
|
||||
[("title", "#{{title}}")]
|
||||
end
|
||||
end
|
||||
|
||||
# apply the metadata parser
|
||||
stream = map_metadata(apply_metadata, stream)
|
||||
|
||||
# define the source. ignore errors and provide no infallibale fallback. yolo.
|
||||
radio = fallback(track_sensitive=false, [stream])
|
||||
|
||||
# transcode to icecast
|
||||
output.icecast(
|
||||
%mp3.vbr(quality=3),
|
||||
name='Croaker Radio',
|
||||
description='Background music for The Frog Hat Club',
|
||||
host="{icecast_host}",
|
||||
port={icecast_port},
|
||||
password="{icecast_password}",
|
||||
mount="{icecast_mount}",
|
||||
icy_metadata="true",
|
||||
url="{icecast_url}",
|
||||
fallible=true,
|
||||
radio
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def generate_liquidsoap_config():
|
||||
log = path.root() / "liquidsoap.log"
|
||||
if log.exists():
|
||||
log.unlink()
|
||||
log.touch()
|
||||
ls_config = path.root() / "croaker.liq"
|
||||
with ls_config.open("wt") as fh:
|
||||
fh.write(
|
||||
LIQUIDSOAP_CONFIG.format(
|
||||
pidfile=_pidfile(terminate_if_running=False),
|
||||
debug_log=log,
|
||||
playlist_root=path.playlist_root(),
|
||||
icecast_host=os.environ.get("ICECAST_HOST"),
|
||||
icecast_port=os.environ.get("ICECAST_PORT"),
|
||||
icecast_mount=os.environ.get("ICECAST_MOUNT"),
|
||||
icecast_password=os.environ.get("ICECAST_PASSWORD"),
|
||||
icecast_url=os.environ.get("ICECAST_URL"),
|
||||
)
|
||||
)
|
||||
path.playlist_root().mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def start_liquidsoap():
|
||||
logging.debug("Staring liquidsoap...")
|
||||
pf = _pidfile(terminate_if_running=False)
|
||||
pid = pf.read_pid()
|
||||
if not pid:
|
||||
logging.info("Liquidsoap does not appear to be running. Starting it...")
|
||||
generate_liquidsoap_config()
|
||||
Popen([os.environ["LIQUIDSOAP"], "--daemon", path.root() / "croaker.liq"])
|
||||
sleep(1)
|
||||
|
||||
|
||||
def start():
|
||||
play_next("session_start")
|
||||
|
||||
|
||||
def stop():
|
||||
_pidfile(terminate_if_running=True)
|
||||
|
||||
|
||||
def play_next(playlist_name: str = None):
|
||||
start_liquidsoap()
|
||||
if playlist_name:
|
||||
pl = load_playlist(playlist_name)
|
||||
logging.debug(f"Loaded playlist {pl = }")
|
||||
if NOW_PLAYING != pl.name:
|
||||
_switch_to(pl)
|
||||
_send_liquidsoap_command("skip")
|
||||
|
||||
|
||||
def _pidfile(terminate_if_running: bool = True):
|
||||
pf = os.environ.get("LIQUIDSOAP_PIDFILE", None)
|
||||
if pf:
|
||||
pf = Path(pf)
|
||||
else:
|
||||
pf = path.root() / "liquidsoap.pid"
|
||||
return pidfile(pf, terminate_if_running=terminate_if_running)
|
||||
|
||||
|
||||
def _switch_to(playlist: Playlist):
|
||||
logging.debug(f"Switching to {playlist = }")
|
||||
np = path.playlist_root() / Path("now_playing")
|
||||
with np.open("wt") as fh:
|
||||
for track in playlist.tracks:
|
||||
fh.write(f"{track}\n")
|
||||
playlist.name
|
||||
|
||||
|
||||
def _send_liquidsoap_command(command: str):
|
||||
conn = Telnet()
|
||||
conn.connect("localhost", port=1234)
|
||||
conn.send(f"Croaker_Radio.{command}\r")
|
||||
conn.send("quit\r")
|
||||
conn.close()
|
16
croaker/exceptions.py
Normal file
16
croaker/exceptions.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
class APIHandlingException(Exception):
|
||||
"""
|
||||
An API reqeust could not be encoded or decoded.
|
||||
"""
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
"""
|
||||
An error was discovered with the Groove on Demand configuration.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidPathError(Exception):
|
||||
"""
|
||||
The specified path was invalid -- either it was not the expected type or wasn't accessible.
|
||||
"""
|
47
croaker/path.py
Normal file
47
croaker/path.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from croaker.exceptions import ConfigurationError
|
||||
|
||||
_setup_hint = "You may be able to solve this error by running 'croaker setup' or specifying the --root parameter."
|
||||
_reinstall_hint = "You might need to reinstall Groove On Demand to fix this error."
|
||||
|
||||
|
||||
def root():
|
||||
return Path(os.environ.get("CROAKER_ROOT", "~/.dnd/croaker")).expanduser()
|
||||
|
||||
|
||||
def media_root():
|
||||
path = os.environ.get("MEDIA_ROOT", None)
|
||||
if not path:
|
||||
raise ConfigurationError(f"MEDIA_ROOT is not defined in your environment.\n\n{_setup_hint}")
|
||||
path = Path(path).expanduser()
|
||||
if not path.exists() or not path.is_dir():
|
||||
raise ConfigurationError(
|
||||
"The media_root directory (MEDIA_ROOT) doesn't exist, or isn't a directory.\n\n{_setup_hint}"
|
||||
)
|
||||
logging.debug(f"Media root is {path}")
|
||||
return path
|
||||
|
||||
|
||||
def cache_root():
|
||||
path = Path(os.environ.get("CACHE_ROOT", root() / Path("cache"))).expanduser()
|
||||
logging.debug(f"Media cache root is {path}")
|
||||
return path
|
||||
|
||||
|
||||
def playlist_root():
|
||||
path = Path(os.environ.get("PLAYLIST_ROOT", root() / Path("playlsits"))).expanduser()
|
||||
logging.debug(f"Playlist root is {path}")
|
||||
return path
|
||||
|
||||
|
||||
def media(relpath):
|
||||
path = media_root() / Path(relpath)
|
||||
return path
|
||||
|
||||
|
||||
def transcoded_media(relpath):
|
||||
path = cache_root() / Path(relpath + ".webm")
|
||||
return path
|
19
croaker/pidfile.py
Normal file
19
croaker/pidfile.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
import logging
|
||||
import os
|
||||
import signal
|
||||
from pathlib import Path
|
||||
|
||||
from daemon import pidfile as _pidfile
|
||||
|
||||
|
||||
def pidfile(pidfile_path: Path, terminate_if_running: bool = True):
|
||||
pf = _pidfile.TimeoutPIDLockFile(pidfile_path, 30)
|
||||
pid = pf.read_pid()
|
||||
if pid and terminate_if_running:
|
||||
try:
|
||||
logging.debug(f"Stopping PID {pid}")
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
logging.debug(f"PID {pid} not running; breaking lock.")
|
||||
pf.break_lock()
|
||||
return pf
|
86
croaker/playlist.py
Normal file
86
croaker/playlist.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from random import shuffle
|
||||
from typing import List
|
||||
|
||||
import croaker.path
|
||||
|
||||
playlists = {}
|
||||
|
||||
|
||||
def _stripped(name):
|
||||
name.replace('"', "")
|
||||
name.replace("'", "")
|
||||
return name
|
||||
|
||||
|
||||
@dataclass
|
||||
class Playlist:
|
||||
name: str
|
||||
theme: Path = Path("_theme.mp3")
|
||||
current_track: int = 0
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
return croaker.path.playlist_root() / Path(self.name)
|
||||
|
||||
@cached_property
|
||||
def tracks(self):
|
||||
if not self.path.exists():
|
||||
raise RuntimeError(f"Playlist {self.name} not found at {self.path}.")
|
||||
|
||||
entries = []
|
||||
theme = self.path / self.theme
|
||||
if theme.exists():
|
||||
entries[0] = theme
|
||||
files = [e for e in self.get_audio_files() if e.name != "_theme.mp3"]
|
||||
if files:
|
||||
shuffle(files)
|
||||
entries += files
|
||||
return entries
|
||||
|
||||
def get_audio_files(self, path: Path = None):
|
||||
if not path:
|
||||
path = self.path
|
||||
logging.debug(f"Getting files matching {os.environ['MEDIA_GLOB']} from {path}")
|
||||
pats = os.environ["MEDIA_GLOB"].split(",")
|
||||
return chain(*[list(path.glob(pat)) for pat in pats])
|
||||
|
||||
def _add_track(self, target: Path, source: Path, make_theme: bool = False):
|
||||
if source.is_dir():
|
||||
for file in self.get_audio_files(source):
|
||||
self._add_track(self.path / _stripped(file.name), file)
|
||||
return
|
||||
if target.exists():
|
||||
if not target.is_symlink():
|
||||
logging.warning(f"{target}: target already exists and is not a symlink; skipping.")
|
||||
return
|
||||
target.unlink()
|
||||
target.symlink_to(source)
|
||||
|
||||
def add(self, tracks: List[Path], make_theme: bool = False):
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
if make_theme:
|
||||
if source.is_dir():
|
||||
raise RuntimeError(f"Cannot create a playlist theme from a directory: {source}")
|
||||
target = self.path / "_theme.mp3"
|
||||
source = tracks.pop(0)
|
||||
self._add_track(target, source, make_theme=True)
|
||||
for track in tracks:
|
||||
self._add_track(target=self.path / _stripped(track.name), source=track)
|
||||
return sorted(self.get_audio_files())
|
||||
|
||||
def __repr__(self):
|
||||
lines = [f"Playlist {self.name}"]
|
||||
lines += [f" * {track}" for track in self.tracks]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def load_playlist(name: str):
|
||||
if name not in playlists:
|
||||
playlists[name] = Playlist(name=name)
|
||||
return playlists[name]
|
17
croaker/routes.py
Normal file
17
croaker/routes.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from bottle import route
|
||||
|
||||
from croaker import controller
|
||||
|
||||
|
||||
@route("/play/<playlist_name>")
|
||||
def play(playlist_name=None):
|
||||
if not controller.play_next(playlist_name):
|
||||
return
|
||||
return "OK"
|
||||
|
||||
|
||||
@route("/skip")
|
||||
def skip():
|
||||
if not controller.play_next():
|
||||
return
|
||||
return "OK"
|
43
croaker/server.py
Normal file
43
croaker/server.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import bottle
|
||||
import daemon
|
||||
|
||||
from croaker import path, routes
|
||||
from croaker.pidfile import pidfile
|
||||
|
||||
assert routes
|
||||
app = bottle.default_app()
|
||||
|
||||
|
||||
def _pidfile(terminate_if_running: bool = True):
|
||||
pf = os.environ.get("PIDFILE", None)
|
||||
if pf:
|
||||
pf = Path(pf)
|
||||
else:
|
||||
pf = path.root() / "croaker.pid"
|
||||
return pidfile(pf, terminate_if_running=terminate_if_running)
|
||||
|
||||
|
||||
def daemonize(host: str = "0.0.0.0", port: int = 8003, debug: bool = False) -> None: # pragma: no cover
|
||||
logging.info(f"Daemonizing webserver on http://{host}:{port}, pidfile and output in {path.root()}")
|
||||
context = daemon.DaemonContext()
|
||||
context.pidfile = _pidfile()
|
||||
context.stdout = open(path.root() / Path("croaker.out"), "wb")
|
||||
context.stderr = open(path.root() / Path("croaker.err"), "wb", buffering=0)
|
||||
context.open()
|
||||
start(host, port, debug)
|
||||
|
||||
|
||||
def stop():
|
||||
_pidfile()
|
||||
|
||||
|
||||
def start(host: str = "0.0.0.0", port: int = 8003, debug: bool = False) -> None: # pragma: no cover
|
||||
"""
|
||||
Start the Bottle app.
|
||||
"""
|
||||
logging.debug(f"Configuring webserver with host={host}, port={port}, debug={debug}")
|
||||
app.run(host=os.getenv("HOST", host), port=os.getenv("PORT", port), debug=debug, server="paste", quiet=True)
|
54
pyproject.toml
Normal file
54
pyproject.toml
Normal file
|
@ -0,0 +1,54 @@
|
|||
[tool.poetry]
|
||||
name = "croaker"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["evilchili <evilchili@gmail.com>"]
|
||||
readme = "README.md"
|
||||
packages = [
|
||||
{ include = "croaker" }
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
prompt-toolkit = "^3.0.38"
|
||||
typer = "^0.9.0"
|
||||
python-dotenv = "^0.21.0"
|
||||
rich = "^13.7.0"
|
||||
pyyaml = "^6.0.1"
|
||||
bottle = "^0.12.25"
|
||||
paste = "^3.7.1"
|
||||
python-daemon = "^3.0.1"
|
||||
requests = "^2.31.0"
|
||||
psutil = "^5.9.8"
|
||||
exscript = "^2.6.28"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
croaker = "croaker.cli:app"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^23.3.0"
|
||||
isort = "^5.12.0"
|
||||
pyproject-autoflake = "^1.0.2"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py310']
|
||||
|
||||
[tool.isort]
|
||||
multi_line_output = 3
|
||||
line_length = 120
|
||||
include_trailing_comma = true
|
||||
|
||||
[tool.autoflake]
|
||||
check = false # return error code if changes are needed
|
||||
in-place = true # make changes to files instead of printing diffs
|
||||
recursive = true # drill down directories recursively
|
||||
remove-all-unused-imports = true # remove all unused imports (not just those from the standard library)
|
||||
ignore-init-module-imports = true # exclude __init__.py when removing unused imports
|
||||
remove-duplicate-keys = true # remove all duplicate keys in objects
|
||||
remove-unused-variables = true # remove unused variables
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
Loading…
Reference in New Issue
Block a user