=====================================================================

--- Changes ------------------------------

- python library functions moved to `py/lib`
- python scwrypts renamed in kebob-case to help prevent import
- __name__ == '__main__' enforced on all python scwrypts

--- New Features -------------------------

- `__override` variables now allow values to be force-overwritten
- py.lib.http.client provides a slim `requests.request` wrapper

--- New Scripts --------------------------

py/data/convert )
  quick data converters
   - csv-to-json
   - csv-to-yaml
   - json-to-csv
   - json-to-yaml
   - yaml-to-csv
   - yaml-to-json

py/linear )
  uses the linear.app graphql API for PM tasks
   - comment

--- Bug Fixes ----------------------------

- `scwrypts` handles arguments with quotes and special characters
This commit is contained in:
Wryn (yage) Wagner 2023-01-11 17:09:59 -07:00
parent a1256bb0af
commit 7617c938b1
35 changed files with 395 additions and 22 deletions

View File

@ -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=

View File

@ -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 |

0
py/data/__init__.py Normal file
View File

View File

21
py/data/convert/csv-to-json.py Executable file
View File

@ -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',
)

21
py/data/convert/csv-to-yaml.py Executable file
View File

@ -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',
)

21
py/data/convert/json-to-csv.py Executable file
View File

@ -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',
)

21
py/data/convert/json-to-yaml.py Executable file
View File

@ -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',
)

21
py/data/convert/yaml-to-csv.py Executable file
View File

@ -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',
)

21
py/data/convert/yaml-to-json.py Executable file
View File

@ -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',
)

19
py/hello-world.py Executable file
View File

@ -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)

View File

@ -1,7 +0,0 @@
#!/usr/bin/env python
def main():
print('HELLO WORLD')
if __name__ == '__main__':
main()

0
py/lib/__init__.py Normal file
View File

0
py/lib/data/__init__.py Normal file
View File

76
py/lib/data/converter.py Normal file
View File

@ -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)

51
py/lib/data/io.py Normal file
View File

@ -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,
)

1
py/lib/http/__init__.py Normal file
View File

@ -0,0 +1 @@
from py.lib.http.client import get_request_client

20
py/lib/http/client.py Normal file
View File

@ -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'
},
)

View File

@ -0,0 +1 @@
from py.lib.linear.client import request, graphql

13
py/lib/linear/client.py Normal file
View File

@ -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})

1
py/lib/redis/__init__.py Normal file
View File

@ -0,0 +1 @@

View File

@ -1,6 +1,6 @@
from redis import StrictRedis
from py.scwrypts import getenv
from py.lib.scwrypts import getenv
class RedisClient(StrictRedis):

View File

@ -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

View File

@ -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):

View File

@ -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')

0
py/linear/__init__.py Normal file
View File

47
py/linear/comment.py Executable file
View File

@ -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)

View File

@ -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()

View File

@ -1,2 +1,3 @@
redis
bpython
pyyaml

View File

@ -1,3 +0,0 @@
from py.scwrypts.getenv import getenv
from py.scwrypts.interactive import interactive
from py.scwrypts.run import run

6
run
View File

@ -102,10 +102,10 @@ __RUN() {
[ ! $LOGFILE ] && {
[ $HEADER ] && echo $HEADER
[ $SUBSCWRYPT ] && {
eval $RUN_STRING $@
eval "$RUN_STRING $(printf "%q " "$@")"
exit $?
} || {
eval $RUN_STRING $@ </dev/tty >/dev/tty 2>&1
eval "$RUN_STRING $(printf "%q " "$@")" </dev/tty >/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'

View File

@ -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.

View File

@ -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"