diff --git a/.env.template b/.env.template index 33bc474..b171248 100644 --- a/.env.template +++ b/.env.template @@ -9,6 +9,7 @@ export I3__BORDER_PIXEL_SIZE= export I3__DMENU_FONT_SIZE= export I3__GLOBAL_FONT_SIZE= export I3__MODEL_CONFIG= +export LINEAR__API_TOKEN= export REDIS_AUTH= export REDIS_HOST= export REDIS_PORT= diff --git a/.env.template.descriptions b/.env.template.descriptions index 705671b..76fe677 100644 --- a/.env.template.descriptions +++ b/.env.template.descriptions @@ -12,6 +12,8 @@ I3__DMENU_FONT_SIZE | I3__GLOBAL_FONT_SIZE | I3__MODEL_CONFIG | +LINEAR__API_TOKEN | linear.app project management configuration + REDIS_AUTH | redis connection credentials REDIS_HOST | REDIS_PORT | diff --git a/py/data/__init__.py b/py/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/data/convert/__init__.py b/py/data/convert/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/data/convert/csv-to-json.py b/py/data/convert/csv-to-json.py new file mode 100755 index 0000000..591a3f1 --- /dev/null +++ b/py/data/convert/csv-to-json.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts csv into json') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'csv', + output_file = args.output_file, + output_type = 'json', + ) diff --git a/py/data/convert/csv-to-yaml.py b/py/data/convert/csv-to-yaml.py new file mode 100755 index 0000000..a0fe70a --- /dev/null +++ b/py/data/convert/csv-to-yaml.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts csv into yaml') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'csv', + output_file = args.output_file, + output_type = 'yaml', + ) diff --git a/py/data/convert/json-to-csv.py b/py/data/convert/json-to-csv.py new file mode 100755 index 0000000..ce8826d --- /dev/null +++ b/py/data/convert/json-to-csv.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts csv into json') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'json', + output_file = args.output_file, + output_type = 'csv', + ) diff --git a/py/data/convert/json-to-yaml.py b/py/data/convert/json-to-yaml.py new file mode 100755 index 0000000..96ea40d --- /dev/null +++ b/py/data/convert/json-to-yaml.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts json into yaml') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'json', + output_file = args.output_file, + output_type = 'yaml', + ) diff --git a/py/data/convert/yaml-to-csv.py b/py/data/convert/yaml-to-csv.py new file mode 100755 index 0000000..108bafa --- /dev/null +++ b/py/data/convert/yaml-to-csv.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts yaml into csv') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'yaml', + output_file = args.output_file, + output_type = 'csv', + ) diff --git a/py/data/convert/yaml-to-json.py b/py/data/convert/yaml-to-json.py new file mode 100755 index 0000000..1a7239d --- /dev/null +++ b/py/data/convert/yaml-to-json.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import add_io_arguments +from py.lib.data.converter import convert + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'converts yaml into json') +add_io_arguments(parser) + +args = parser.parse_args() + +convert( + input_file = args.input_file, + input_type = 'yaml', + output_file = args.output_file, + output_type = 'json', + ) diff --git a/py/hello-world.py b/py/hello-world.py new file mode 100755 index 0000000..26090b8 --- /dev/null +++ b/py/hello-world.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'a simple "Hello, World!" program') +parser.add_argument( + '-m', '--message', + dest = 'message', + default = 'HELLO WORLD', + help = 'message to print to stdout', + required = False, + ) + +args = parser.parse_args() + +print(args.message) diff --git a/py/hello_world.py b/py/hello_world.py deleted file mode 100755 index 346aec1..0000000 --- a/py/hello_world.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python - -def main(): - print('HELLO WORLD') - -if __name__ == '__main__': - main() diff --git a/py/lib/__init__.py b/py/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/lib/data/__init__.py b/py/lib/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/lib/data/converter.py b/py/lib/data/converter.py new file mode 100644 index 0000000..92edb04 --- /dev/null +++ b/py/lib/data/converter.py @@ -0,0 +1,76 @@ +import csv +import json +import yaml + +from py.lib.data.io import get_stream + + +def convert(input_file, input_type, output_file, output_type): + if input_type == output_type: + raise ValueError('input type and output type are the same') + + with get_stream(input_file) as input_stream: + data = convert_input(input_stream, input_type) + + with get_stream(output_file, 'w+') as output_stream: + _write_output(output_stream, output_type, data) + + +def convert_input(stream, input_type): + supported_input_types = {'csv', 'json', 'yaml'} + + if input_type not in supported_input_types: + raise ValueError(f'input_type "{input_type}" not supported; must be one of {supported_input_types}') + + return { + 'csv': _read_csv, + 'json': _read_json, + 'yaml': _read_yaml, + }[input_type](stream) + +def _write_output(stream, output_type, data): + supported_output_types = {'csv', 'json', 'yaml'} + + if output_type not in supported_output_types: + raise ValueError(f'output_type "{output_type}" not supported; must be one of {supported_output_types}') + + return { + 'csv': _write_csv, + 'json': _write_json, + 'yaml': _write_yaml, + }[output_type](stream, data) + +##################################################################### + +def _read_csv(stream): + return [dict(line) for line in csv.DictReader(stream)] + +def _write_csv(stream, data): + writer = csv.DictWriter(stream, fieldnames=list({ + key + for dictionary in data + for key in dictionary.keys() + })) + + writer.writeheader() + + for value in data: + writer.writerow(value) + +##################################################################### + +def _read_json(stream): + data = json.loads(stream.read()) + return data if isinstance(data, list) else [data] + +def _write_json(stream, data): + stream.write(json.dumps(data)) + +##################################################################### + +def _read_yaml(stream): + data = yaml.safe_load(stream) + return data if isinstance(data, list) else [data] + +def _write_yaml(stream, data): + yaml.dump(data, stream, default_flow_style=False) diff --git a/py/lib/data/io.py b/py/lib/data/io.py new file mode 100644 index 0000000..65ccf55 --- /dev/null +++ b/py/lib/data/io.py @@ -0,0 +1,51 @@ +from contextlib import contextmanager +from pathlib import Path +from sys import stdin, stdout, stderr + +from py.lib.scwrypts.getenv import getenv + + +@contextmanager +def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwargs): + allowed_modes = {'r', 'w', 'w+'} + + if mode not in allowed_modes: + raise ValueError(f'mode "{mode}" not supported modes (must be one of {allowed_modes})') + + is_read = mode == 'r' + + if filename is not None: + + if verbose: + print(f'opening file {filename} for {"read" if is_read else "write"}', file=stderr) + + if filename[0] not in {'/', '~'}: + filename = Path(f'{getenv("EXECUTION_DIR")}/{filename}').resolve() + with open(filename, mode=mode, encoding=encoding, **kwargs) as stream: + yield stream + + else: + if verbose: + print('using stdin for read' if is_read else 'using stdout for write', file=stderr) + + yield stdin if is_read else stdout + + +def add_io_arguments(parser, toggle_input=True, toggle_output=True): + if toggle_input: + parser.add_argument( + '-i', '--input-file', + dest = 'input_file', + default = None, + help = 'path to input file; omit for stdin', + required = False, + ) + + if toggle_output: + parser.add_argument( + '-o', '--output-file', + dest = 'output_file', + default = None, + help = 'path to output file; omit for stdout', + required = False, + ) diff --git a/py/lib/http/__init__.py b/py/lib/http/__init__.py new file mode 100644 index 0000000..6df13ed --- /dev/null +++ b/py/lib/http/__init__.py @@ -0,0 +1 @@ +from py.lib.http.client import get_request_client diff --git a/py/lib/http/client.py b/py/lib/http/client.py new file mode 100644 index 0000000..977774f --- /dev/null +++ b/py/lib/http/client.py @@ -0,0 +1,20 @@ +from requests import request + + +def get_request_client(base_url, headers=None): + if headers is None: + headers = {} + + return lambda method, endpoint, **kwargs: request( + method = method, + url = f'{base_url}/{endpoint}', + headers = { + **headers, + **kwargs.get('headers', {}), + }, + **{ + key: value + for key, value in kwargs.items() + if key != 'headers' + }, + ) diff --git a/py/lib/linear/__init__.py b/py/lib/linear/__init__.py new file mode 100644 index 0000000..c600274 --- /dev/null +++ b/py/lib/linear/__init__.py @@ -0,0 +1 @@ +from py.lib.linear.client import request, graphql diff --git a/py/lib/linear/client.py b/py/lib/linear/client.py new file mode 100644 index 0000000..1e8ce1f --- /dev/null +++ b/py/lib/linear/client.py @@ -0,0 +1,13 @@ +from py.lib.http import get_request_client +from py.lib.scwrypts import getenv + + +request = get_request_client( + base_url = 'https://api.linear.app', + headers = { + 'Authorization': f'bearer {getenv("LINEAR__API_TOKEN")}', + } + ) + +def graphql(query): + return request('POST', 'graphql', json={'query': query}) diff --git a/py/lib/redis/__init__.py b/py/lib/redis/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/py/lib/redis/__init__.py @@ -0,0 +1 @@ + diff --git a/py/redis/client.py b/py/lib/redis/client.py similarity index 90% rename from py/redis/client.py rename to py/lib/redis/client.py index 8184be2..1e88a0d 100644 --- a/py/redis/client.py +++ b/py/lib/redis/client.py @@ -1,6 +1,6 @@ from redis import StrictRedis -from py.scwrypts import getenv +from py.lib.scwrypts import getenv class RedisClient(StrictRedis): diff --git a/py/lib/scwrypts/__init__.py b/py/lib/scwrypts/__init__.py new file mode 100644 index 0000000..9245df2 --- /dev/null +++ b/py/lib/scwrypts/__init__.py @@ -0,0 +1,3 @@ +from py.lib.scwrypts.getenv import getenv +from py.lib.scwrypts.interactive import interactive +from py.lib.scwrypts.run import run diff --git a/py/scwrypts/exceptions.py b/py/lib/scwrypts/exceptions.py similarity index 100% rename from py/scwrypts/exceptions.py rename to py/lib/scwrypts/exceptions.py diff --git a/py/scwrypts/getenv.py b/py/lib/scwrypts/getenv.py similarity index 74% rename from py/scwrypts/getenv.py rename to py/lib/scwrypts/getenv.py index c82f512..4fbfbd0 100644 --- a/py/scwrypts/getenv.py +++ b/py/lib/scwrypts/getenv.py @@ -1,7 +1,7 @@ from os import getenv as os_getenv -from py.scwrypts.exceptions import MissingVariableError -from py.scwrypts.run import run +from py.lib.scwrypts.exceptions import MissingVariableError +from py.lib.scwrypts.run import run def getenv(name, required=True): diff --git a/py/scwrypts/interactive.py b/py/lib/scwrypts/interactive.py similarity index 100% rename from py/scwrypts/interactive.py rename to py/lib/scwrypts/interactive.py diff --git a/py/scwrypts/run.py b/py/lib/scwrypts/run.py similarity index 69% rename from py/scwrypts/run.py rename to py/lib/scwrypts/run.py index 9bf8dd2..2b8d308 100644 --- a/py/scwrypts/run.py +++ b/py/lib/scwrypts/run.py @@ -7,11 +7,15 @@ def run(scwrypt_name, *args): DEPTH = int(getenv('SUBSCWRYPT', '0')) DEPTH += 1 + SCWRYPTS_EXE = Path(__file__).parents[2] / 'scwrypts' + ARGS = ' '.join([str(x) for x in args]) + print(f'\n {"--"*DEPTH} ({DEPTH}) BEGIN SUBSCWRYPT : {Path(scwrypt_name).name}') subprocess_run( - f'SUBSCWRYPT={DEPTH} {Path(__file__).parents[2] / "scwrypts"} {scwrypt_name} -- {" ".join([str(x) for x in args])}', + f'SUBSCWRYPT={DEPTH} {SCWRYPTS_EXE} {scwrypt_name} -- {ARGS}', shell=True, executable='/bin/zsh', + check=False, ) print(f' {"--"*DEPTH} ({DEPTH}) END SUBSCWRYPT : {Path(scwrypt_name).name}\n') diff --git a/py/linear/__init__.py b/py/linear/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/linear/comment.py b/py/linear/comment.py new file mode 100755 index 0000000..5a13fee --- /dev/null +++ b/py/linear/comment.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +from argparse import ArgumentParser + +from py.lib.data.io import get_stream, add_io_arguments +from py.lib.linear import graphql + +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'comment on an issue in linear.app') + +parser.add_argument( + '-i', '--issue', + dest = 'issue_id', + help = 'issue short-code (e.g. CLOUD-319)', + required = True, + ) + +parser.add_argument( + '-m', '--message', + dest = 'message', + help = 'comment to post to the target issue', + required = True, + ) + +add_io_arguments(parser, toggle_input=False) + +args = parser.parse_args() + +query = f''' +mutation CommentCreate {{ + commentCreate( + input: {{ + issueId: "{args.issue_id}" + body: """from wrobot: +``` +{args.message.strip()} +```""" + }} + ) {{ success }} +}} +''' + +response = graphql(query) +with get_stream(args.output_file, 'w+') as output: + output.write(response.text) diff --git a/py/redis/interactive.py b/py/redis/interactive.py index 51f84cd..b5bcac8 100755 --- a/py/redis/interactive.py +++ b/py/redis/interactive.py @@ -1,11 +1,19 @@ #!/usr/bin/env python +from argparse import ArgumentParser -from py.redis.client import Client -from py.scwrypts import interactive, getenv +from py.lib.redis.client import Client +from py.lib.scwrypts import interactive, getenv +if __name__ != '__main__': + raise Exception('executable only; must run through scwrypts') + + +parser = ArgumentParser(description = 'establishes a redis client in an interactive python shell') +args = parser.parse_args() @interactive def main(): + # pylint: disable=possibly-unused-variable r = Client print(f''' @@ -14,6 +22,4 @@ def main(): return locals() - -if __name__ == '__main__': - main() +main() diff --git a/py/requirements.txt b/py/requirements.txt index c3ad044..6e7b69d 100644 --- a/py/requirements.txt +++ b/py/requirements.txt @@ -1,2 +1,3 @@ redis bpython +pyyaml diff --git a/py/scwrypts/__init__.py b/py/scwrypts/__init__.py deleted file mode 100644 index 9f5e6a0..0000000 --- a/py/scwrypts/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from py.scwrypts.getenv import getenv -from py.scwrypts.interactive import interactive -from py.scwrypts.run import run diff --git a/run b/run index 96040ec..175d06d 100755 --- a/run +++ b/run @@ -102,10 +102,10 @@ __RUN() { [ ! $LOGFILE ] && { [ $HEADER ] && echo $HEADER [ $SUBSCWRYPT ] && { - eval $RUN_STRING $@ + eval "$RUN_STRING $(printf "%q " "$@")" exit $? } || { - eval $RUN_STRING $@ /dev/tty 2>&1 + eval "$RUN_STRING $(printf "%q " "$@")" /dev/tty 2>&1 exit $? } } @@ -113,7 +113,7 @@ __RUN() { { [ $HEADER ] && echo $HEADER echo '\033[1;33m--- BEGIN OUTPUT -------------------------\033[0m' - eval $RUN_STRING $@ + eval "$RUN_STRING $(printf "%q " "$@")" EXIT_CODE=$? echo '\033[1;33m--- END OUTPUT ---------------------------\033[0m' diff --git a/zsh/scwrypts/README.md b/zsh/scwrypts/README.md index f15c513..d6e3337 100644 --- a/zsh/scwrypts/README.md +++ b/zsh/scwrypts/README.md @@ -50,6 +50,14 @@ Setting the `AWS_REGION` variable will cause scwrypts to ignore the `__select` s CI will fail on select, because CI fails on any FZF prompt. +#### `__override` Environment Variables +Override any variable with the indicated value. +This will take precedence over existing values *and* any other special environment variable types. + +Examples of use: +- temporarily changing a single value in your current session (e.g. `export VARIABLE__override=value`) +- overriding a variable for a one-time command (e.g. `VARIABLE__override=value scwrypts ...`) + ## Logs Quickly view or clear Scwrypts logs. diff --git a/zsh/utils/environment.zsh b/zsh/utils/environment.zsh index 84dde90..8332800 100644 --- a/zsh/utils/environment.zsh +++ b/zsh/utils/environment.zsh @@ -8,6 +8,9 @@ __CHECK_ENV_VAR() { local NAME="$1" [ ! $NAME ] && return 1 + local OVERRIDE_VALUE=$(eval echo '$'$NAME'__override') + [ $OVERRIDE_VALUE ] && export $NAME=$OVERRIDE_VALUE && return 0 + local OPTIONAL="$2" local DEFAULT_VALUE="$3"