v3.0.0 "The Great Overhaul"
===================================================================== Notice the major version change which comes with breaking changes to 2.x! Reconstructs "library" functions for both python and zsh scwrypts, with changes to virtualenv naming conventions (you'll need to refresh all virtualenv with the appropriate scwrypt). --- Changes ------------------------------ - changed a naming convention across zsh scripts, particularly removing underscores where there is no need to avoid naming clash (e.g. 'zsh/lib/utils/io.zsh' renames '__STATUS' to 'STATUS') - moved clients reliant on py.lib.http to the py.lib.http module - python scripts now rely on py.lib.scwrypts.execute - updated package.json in zx scripts to include `type = module` - 'scwrypts --list' commandline argument now includes additional relevant data for each scwrypt - environment variables no longer add themselves to be staged in the '.env.template' --- New Features ------------------------- - new 'use' syntax for disjoint import within zsh scripts; took me a very long time to convince myself this would be necessary - introduced scwrypt "groups" to allow portable module creation; (i.e. ability add your own scripts from another repo!) - py.lib.scwrypts.io provides a combined IO stream for quick, hybrid use of input/output files and stdin/stdout - py.lib.fzf provides a wrapper to provide similar functionality to zsh/utils/io.zsh including fzf_(head|tail) - improved efficiency of various scwrypts; notably reducing runtime of scwrypts/environment sync - improved scwrypts CLI by adding new options for exact scwrypt matching, better filtering, and prettier/more-detailed interfaces --- New Scripts -------------------------- - py/twilio ) basic SMS integration with twilio - send-sms - py/directus ) interactive directus GET query - get-items - py/discord ) post message to discord channel or webhook - post-message
This commit is contained in:
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts csv into json')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'csv',
|
||||
output_stream = stream.output,
|
||||
output_type = 'json',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'csv',
|
||||
output_file = args.output_file,
|
||||
output_type = 'json',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert csv into json',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts csv into yaml')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'csv',
|
||||
output_stream = stream.output,
|
||||
output_type = 'yaml',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'csv',
|
||||
output_file = args.output_file,
|
||||
output_type = 'yaml',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert csv into yaml',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts csv into json')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'json',
|
||||
output_stream = stream.output,
|
||||
output_type = 'csv',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'json',
|
||||
output_file = args.output_file,
|
||||
output_type = 'csv',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert json into csv',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts json into yaml')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'json',
|
||||
output_stream = stream.output,
|
||||
output_type = 'yaml',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'json',
|
||||
output_file = args.output_file,
|
||||
output_type = 'yaml',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert json into yaml',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts yaml into csv')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'yaml',
|
||||
output_stream = stream.output,
|
||||
output_type = 'csv',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'yaml',
|
||||
output_file = args.output_file,
|
||||
output_type = 'csv',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert yaml into csv',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,21 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from py.lib.data.io import add_io_arguments
|
||||
from py.lib.data.converter import convert
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
parser = ArgumentParser(description = 'converts yaml into json')
|
||||
add_io_arguments(parser)
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'yaml',
|
||||
output_stream = stream.output,
|
||||
output_type = 'json',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
convert(
|
||||
input_file = args.input_file,
|
||||
input_type = 'yaml',
|
||||
output_file = args.output_file,
|
||||
output_type = 'json',
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'convert yaml into json',
|
||||
parse_args = [],
|
||||
)
|
||||
|
0
py/directus/__init__.py
Normal file
0
py/directus/__init__.py
Normal file
145
py/directus/get-items.py
Executable file
145
py/directus/get-items.py
Executable file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
from json import dumps
|
||||
|
||||
from py.lib.fzf import fzf, fzf_tail
|
||||
from py.lib.http import directus
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
def main(args, stream):
|
||||
if {None} == { args.collection, args.filters, args.fields }:
|
||||
args.interactive = True
|
||||
|
||||
if args.interactive:
|
||||
args.generate_filters_prompt = True
|
||||
args.generate_fields_prompt = True
|
||||
|
||||
collection = _get_or_select_collection(args)
|
||||
filters = _get_or_select_filters(args, collection)
|
||||
fields = _get_or_select_fields(args, collection)
|
||||
|
||||
query = '&'.join([
|
||||
param for param in [
|
||||
fields,
|
||||
filters,
|
||||
]
|
||||
if param
|
||||
])
|
||||
|
||||
endpoint = f'items/{collection}?{query}'
|
||||
|
||||
response = directus.request('GET', endpoint)
|
||||
|
||||
stream.writeline(dumps({
|
||||
**response.json(),
|
||||
'scwrypts_metadata': {
|
||||
'endpoint': endpoint,
|
||||
'repeat_with': f'scwrypts -n py/directus/get-items -- -c {collection} -f \'{query}\'',
|
||||
},
|
||||
}))
|
||||
|
||||
def _get_or_select_collection(args):
|
||||
collection = args.collection
|
||||
|
||||
if collection is None:
|
||||
collection = fzf(
|
||||
prompt = 'select a collection',
|
||||
choices = directus.get_collections(),
|
||||
)
|
||||
|
||||
if not collection:
|
||||
raise ValueError('collection required for query')
|
||||
|
||||
return collection
|
||||
|
||||
def _get_or_select_filters(args, collection):
|
||||
filters = args.filters or ''
|
||||
|
||||
if filters == '' and args.generate_filters_prompt:
|
||||
filters = '&'.join([
|
||||
f'filter[{filter}][' + (
|
||||
operator := fzf(
|
||||
prompt = f'select operator for {filter}',
|
||||
choices = directus.FILTER_OPERATORS,
|
||||
)
|
||||
) + ']=' + fzf_tail(prompt = f'filter[{filter}][{operator}]')
|
||||
|
||||
for filter in fzf(
|
||||
prompt = 'select filter(s) [C^c to skip]',
|
||||
fzf_options = '--multi',
|
||||
force_list = True,
|
||||
choices = directus.get_fields(collection),
|
||||
)
|
||||
])
|
||||
|
||||
return filters
|
||||
|
||||
def _get_or_select_fields(args, collection):
|
||||
fields = args.fields or ''
|
||||
|
||||
if fields == '' and args.generate_fields_prompt:
|
||||
fields = ','.join(fzf(
|
||||
prompt = 'select return field(s) [C^c to get all]',
|
||||
fzf_options = '--multi',
|
||||
choices = directus.get_fields(collection),
|
||||
force_list = True,
|
||||
))
|
||||
|
||||
if fields:
|
||||
fields = f'fields[]={fields}'
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'interactive CLI to get data from directus',
|
||||
parse_args = [
|
||||
( ['-c', '--collection'], {
|
||||
"dest" : 'collection',
|
||||
"default" : None,
|
||||
"help" : 'the name of the collection',
|
||||
"required" : False,
|
||||
}),
|
||||
( ['-f', '--filters'], {
|
||||
"dest" : 'filters',
|
||||
"default" : None,
|
||||
"help" : 'as a URL-suffix, filters for the query',
|
||||
"required" : False,
|
||||
}),
|
||||
( ['-d', '--fields'], {
|
||||
"dest" : 'fields',
|
||||
"default" : None,
|
||||
"help" : 'comma-separated list of fields to include',
|
||||
"required" : False,
|
||||
}),
|
||||
( ['-p', '--interactive-prompt'], {
|
||||
"action" : 'store_true',
|
||||
"dest" : 'interactive',
|
||||
"default" : False,
|
||||
"help" : 'interactively generate filter prompts; implied if no flags are provided',
|
||||
"required" : False,
|
||||
}),
|
||||
( ['--prompt-filters'], {
|
||||
"action" : 'store_true',
|
||||
"dest" : 'generate_filters_prompt',
|
||||
"default" : False,
|
||||
"help" : '(superceded by -p) only generate filters interactively',
|
||||
"required" : False,
|
||||
}),
|
||||
( ['--prompt-fields'], {
|
||||
"action" : 'store_true',
|
||||
"dest" : 'generate_fields_prompt',
|
||||
"default" : False,
|
||||
"help" : '(superceded by -p) only generate filters interactively',
|
||||
"required" : False,
|
||||
}),
|
||||
]
|
||||
|
||||
)
|
0
py/discord/__init__.py
Normal file
0
py/discord/__init__.py
Normal file
61
py/discord/post-message.py
Executable file
61
py/discord/post-message.py
Executable file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
from json import dumps
|
||||
from sys import stderr
|
||||
|
||||
from py.lib.http import discord
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
def main(args, stream):
|
||||
if args.body is None:
|
||||
print(f'reading input from {stream.input.name}', file=stderr)
|
||||
args.body = ''.join(stream.readlines()).strip()
|
||||
|
||||
if len(args.body) == 0:
|
||||
args.body = 'PING'
|
||||
|
||||
response = discord.send_message(
|
||||
content = args.body,
|
||||
channel_id = args.channel_id,
|
||||
webhook = args.webhook,
|
||||
avatar_url = args.avatar_url,
|
||||
)
|
||||
|
||||
stream.writeline(dumps({
|
||||
**(response.json() if response.text != '' else {'message': 'OK'}),
|
||||
'scwrypts_metadata': {},
|
||||
}))
|
||||
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'post a message to the indicated discord channel',
|
||||
parse_args = [
|
||||
( ['-b', '--body'], {
|
||||
'dest' : 'body',
|
||||
'help' : 'message body',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['-c', '--channel-id'], {
|
||||
'dest' : 'channel_id',
|
||||
'help' : 'target channel id',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['-w', '--webhook'], {
|
||||
'dest' : 'webhook',
|
||||
'help' : 'target webhook (takes precedence over -c)',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['--avatar-url'], {
|
||||
'dest' : 'avatar_url',
|
||||
'help' : 'replace default avatar_url',
|
||||
'required' : False,
|
||||
}),
|
||||
]
|
||||
)
|
@ -1,19 +1,27 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
|
||||
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,
|
||||
def main(args, stream):
|
||||
stream.writeline(args.message)
|
||||
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'a simple "Hello, World!" program',
|
||||
parse_args = [
|
||||
( ['-m', '--message'], {
|
||||
'dest' : 'message',
|
||||
'default' : 'HELLO WORLD',
|
||||
'help' : 'message to print',
|
||||
'required' : False,
|
||||
}),
|
||||
],
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(args.message)
|
||||
|
@ -0,0 +1,6 @@
|
||||
import py.lib.data
|
||||
import py.lib.fzf
|
||||
import py.lib.http
|
||||
import py.lib.redis
|
||||
import py.lib.scwrypts
|
||||
import py.lib.twilio
|
||||
|
@ -0,0 +1 @@
|
||||
import py.lib.data.converter
|
||||
|
@ -2,18 +2,13 @@ import csv
|
||||
import json
|
||||
import yaml
|
||||
|
||||
from py.lib.data.io import get_stream
|
||||
|
||||
|
||||
def convert(input_file, input_type, output_file, output_type):
|
||||
def convert(input_stream, input_type, output_stream, 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)
|
||||
data = convert_input(input_stream, input_type)
|
||||
write_output(output_stream, output_type, data)
|
||||
|
||||
|
||||
def convert_input(stream, input_type):
|
||||
@ -28,7 +23,8 @@ def convert_input(stream, input_type):
|
||||
'yaml': _read_yaml,
|
||||
}[input_type](stream)
|
||||
|
||||
def _write_output(stream, output_type, data):
|
||||
|
||||
def write_output(stream, output_type, data):
|
||||
supported_output_types = {'csv', 'json', 'yaml'}
|
||||
|
||||
if output_type not in supported_output_types:
|
||||
@ -40,6 +36,7 @@ def _write_output(stream, output_type, data):
|
||||
'yaml': _write_yaml,
|
||||
}[output_type](stream, data)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
def _read_csv(stream):
|
||||
|
1
py/lib/fzf/__init__.py
Normal file
1
py/lib/fzf/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from py.lib.fzf.client import fzf, fzf_tail, fzf_head
|
61
py/lib/fzf/client.py
Normal file
61
py/lib/fzf/client.py
Normal file
@ -0,0 +1,61 @@
|
||||
from pyfzf.pyfzf import FzfPrompt
|
||||
|
||||
FZF_PROMPT = None
|
||||
|
||||
|
||||
def fzf( # pylint: disable=too-many-arguments
|
||||
choices=None,
|
||||
prompt=None,
|
||||
fzf_options='',
|
||||
delimiter='\n',
|
||||
return_type=str,
|
||||
force_list=False,
|
||||
):
|
||||
global FZF_PROMPT # pylint: disable=global-statement
|
||||
|
||||
if choices is None:
|
||||
choices = []
|
||||
|
||||
if not isinstance(return_type, type):
|
||||
raise ValueError(f'return_type must be a valid python type; "{return_type}" is not a type')
|
||||
|
||||
if FZF_PROMPT is None:
|
||||
FZF_PROMPT = FzfPrompt()
|
||||
|
||||
options = ' '.join({
|
||||
'-i',
|
||||
'--layout=reverse',
|
||||
'--ansi',
|
||||
'--height=30%',
|
||||
f'--prompt "{prompt} : "' if prompt is not None else '',
|
||||
fzf_options,
|
||||
})
|
||||
|
||||
selections = [
|
||||
return_type(selection)
|
||||
for selection in FZF_PROMPT.prompt(choices, options, delimiter)
|
||||
]
|
||||
|
||||
if not force_list:
|
||||
if len(selections) == 0:
|
||||
return None
|
||||
|
||||
if len(selections) == 1:
|
||||
return selections[0]
|
||||
|
||||
return selections
|
||||
|
||||
|
||||
def fzf_tail(*args, **kwargs):
|
||||
return _fzf_print(*args, **kwargs)[-1]
|
||||
|
||||
def fzf_head(*args, **kwargs):
|
||||
return _fzf_print(*args, **kwargs)[0]
|
||||
|
||||
def _fzf_print(*args, fzf_options='', **kwargs):
|
||||
return fzf(
|
||||
*args,
|
||||
**kwargs,
|
||||
fzf_options = f'--print-query {fzf_options}',
|
||||
force_list = True,
|
||||
)
|
@ -1 +1,5 @@
|
||||
from py.lib.http.client import get_request_client
|
||||
|
||||
import py.lib.http.directus
|
||||
import py.lib.http.discord
|
||||
import py.lib.http.linear
|
||||
|
2
py/lib/http/directus/__init__.py
Normal file
2
py/lib/http/directus/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from py.lib.http.directus.client import *
|
||||
from py.lib.http.directus.constant import *
|
56
py/lib/http/directus/client.py
Normal file
56
py/lib/http/directus/client.py
Normal file
@ -0,0 +1,56 @@
|
||||
from py.lib.http import get_request_client
|
||||
from py.lib.scwrypts import getenv
|
||||
|
||||
|
||||
REQUEST = None
|
||||
COLLECTIONS = None
|
||||
FIELDS = {}
|
||||
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
global REQUEST # pylint: disable=global-statement
|
||||
|
||||
if REQUEST is None:
|
||||
REQUEST = get_request_client(
|
||||
base_url = getenv("DIRECTUS__BASE_URL"),
|
||||
headers = {
|
||||
'Authorization': f'bearer {getenv("DIRECTUS__API_TOKEN")}',
|
||||
}
|
||||
)
|
||||
|
||||
return REQUEST(method, endpoint, **kwargs)
|
||||
|
||||
def graphql(query, system=False):
|
||||
return request(
|
||||
'POST',
|
||||
'graphql' if system is True else 'graphql/system',
|
||||
json={'query': query},
|
||||
)
|
||||
|
||||
|
||||
def get_collections():
|
||||
global COLLECTIONS # pylint: disable=global-statement
|
||||
|
||||
if COLLECTIONS is None:
|
||||
COLLECTIONS = [
|
||||
item['collection']
|
||||
for item in request(
|
||||
'GET',
|
||||
'collections?limit=-1&fields[]=collection',
|
||||
).json()['data']
|
||||
]
|
||||
|
||||
return COLLECTIONS
|
||||
|
||||
|
||||
def get_fields(collection):
|
||||
if FIELDS.get(collection) is None:
|
||||
FIELDS[collection] = [
|
||||
item['field']
|
||||
for item in request(
|
||||
'GET',
|
||||
f'fields/{collection}?limit=-1&fields[]=field',
|
||||
).json()['data']
|
||||
]
|
||||
|
||||
return FIELDS[collection]
|
25
py/lib/http/directus/constant.py
Normal file
25
py/lib/http/directus/constant.py
Normal file
@ -0,0 +1,25 @@
|
||||
FILTER_OPERATORS = {
|
||||
'_eq',
|
||||
'_neq',
|
||||
'_lt',
|
||||
'_lte',
|
||||
'_gt',
|
||||
'_gte',
|
||||
'_in',
|
||||
'_nin',
|
||||
'_null',
|
||||
'_nnull',
|
||||
'_contains',
|
||||
'_ncontains',
|
||||
'_starts_with',
|
||||
'_ends_with',
|
||||
'_nends_with',
|
||||
'_between',
|
||||
'_nbetween',
|
||||
'_empty',
|
||||
'_nempty',
|
||||
'_intersects',
|
||||
'_nintersects',
|
||||
'_intersects_bbox',
|
||||
'_nintersects_bbox',
|
||||
}
|
2
py/lib/http/discord/__init__.py
Normal file
2
py/lib/http/discord/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from py.lib.http.discord.client import *
|
||||
from py.lib.http.discord.send_message import *
|
20
py/lib/http/discord/client.py
Normal file
20
py/lib/http/discord/client.py
Normal file
@ -0,0 +1,20 @@
|
||||
from py.lib.http import get_request_client
|
||||
from py.lib.scwrypts import getenv
|
||||
|
||||
REQUEST = None
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
global REQUEST # pylint: disable=global-statement
|
||||
|
||||
if REQUEST is None:
|
||||
headers = {}
|
||||
|
||||
if (token := getenv("DISCORD__BOT_TOKEN", required = False)) is not None:
|
||||
headers['Authorization'] = f'Bot {token}'
|
||||
|
||||
REQUEST = get_request_client(
|
||||
base_url = 'https://discord.com/api',
|
||||
headers = headers,
|
||||
)
|
||||
|
||||
return REQUEST(method, endpoint, **kwargs)
|
34
py/lib/http/discord/send_message.py
Normal file
34
py/lib/http/discord/send_message.py
Normal file
@ -0,0 +1,34 @@
|
||||
from py.lib.scwrypts import getenv
|
||||
from py.lib.http.discord import request
|
||||
|
||||
def send_message(content, channel_id=None, webhook=None, avatar_url=None, **kwargs):
|
||||
if channel_id is None:
|
||||
channel_id = getenv('DISCORD__DEFAULT_CHANNEL_ID', required=False)
|
||||
|
||||
if avatar_url is None:
|
||||
avatar_url = getenv('DISCORD__DEFAULT_AVATAR_URL', required=False)
|
||||
|
||||
endpoint = None
|
||||
|
||||
if webhook is not None:
|
||||
endpoint = f'webhooks/{webhook}'
|
||||
elif channel_id is not None:
|
||||
endpoint = f'channels/{channel_id}/messages'
|
||||
else:
|
||||
raise ValueError('must provide target channel_id or webhook')
|
||||
|
||||
|
||||
return request(
|
||||
method = 'POST',
|
||||
endpoint = endpoint,
|
||||
json = {
|
||||
key: value
|
||||
for key, value in {
|
||||
'content': content,
|
||||
'username': 'wrobot',
|
||||
'avatar_url': avatar_url,
|
||||
**kwargs,
|
||||
}.items()
|
||||
if value is not None
|
||||
},
|
||||
)
|
1
py/lib/http/linear/__init__.py
Normal file
1
py/lib/http/linear/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from py.lib.http.linear.client import *
|
20
py/lib/http/linear/client.py
Normal file
20
py/lib/http/linear/client.py
Normal file
@ -0,0 +1,20 @@
|
||||
from py.lib.http import get_request_client
|
||||
from py.lib.scwrypts import getenv
|
||||
|
||||
REQUEST = None
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
global REQUEST # pylint: disable=global-statement
|
||||
|
||||
if REQUEST is None:
|
||||
REQUEST = get_request_client(
|
||||
base_url = 'https://api.linear.app',
|
||||
headers = {
|
||||
'Authorization': f'bearer {getenv("LINEAR__API_TOKEN")}',
|
||||
}
|
||||
)
|
||||
|
||||
return REQUEST(method, endpoint, **kwargs)
|
||||
|
||||
def graphql(query):
|
||||
return request('POST', 'graphql', json={'query': query})
|
@ -1 +0,0 @@
|
||||
from py.lib.linear.client import request, graphql
|
@ -1,13 +0,0 @@
|
||||
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 +1 @@
|
||||
|
||||
from py.lib.redis.client import get_client
|
||||
|
@ -2,14 +2,18 @@ from redis import StrictRedis
|
||||
|
||||
from py.lib.scwrypts import getenv
|
||||
|
||||
CLIENT = None
|
||||
|
||||
class RedisClient(StrictRedis):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
def get_client():
|
||||
global CLIENT # pylint: disable=global-statement
|
||||
|
||||
if CLIENT is None:
|
||||
print('getting redis client')
|
||||
CLIENT = StrictRedis(
|
||||
host = getenv('REDIS_HOST'),
|
||||
port = getenv('REDIS_PORT'),
|
||||
password = getenv('REDIS_AUTH', required=False),
|
||||
decode_responses = True,
|
||||
)
|
||||
|
||||
Client = RedisClient()
|
||||
return CLIENT
|
||||
|
@ -1,3 +1,6 @@
|
||||
from py.lib.scwrypts.execute import execute
|
||||
from py.lib.scwrypts.getenv import getenv
|
||||
from py.lib.scwrypts.interactive import interactive
|
||||
from py.lib.scwrypts.run import run
|
||||
|
||||
import py.lib.scwrypts.io
|
||||
|
@ -1,3 +1,16 @@
|
||||
class MissingVariableError(Exception):
|
||||
from argparse import ArgumentError
|
||||
|
||||
|
||||
class MissingVariableError(EnvironmentError):
|
||||
def init(self, name):
|
||||
super().__init__(f'Missing required environment variable "{name}"')
|
||||
|
||||
|
||||
class ImportedExecutableError(ImportError):
|
||||
def __init__(self):
|
||||
super().__init__('executable only; must run through scwrypts')
|
||||
|
||||
|
||||
class MissingFlagAndEnvironmentVariableError(EnvironmentError, ArgumentError):
|
||||
def __init__(self, flags, env_var):
|
||||
super().__init__(f'must provide at least one of : {{ flags: {flags} OR {env_var} }}')
|
||||
|
23
py/lib/scwrypts/execute.py
Normal file
23
py/lib/scwrypts/execute.py
Normal file
@ -0,0 +1,23 @@
|
||||
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
||||
|
||||
from py.lib.scwrypts.io import get_combined_stream, add_io_arguments
|
||||
|
||||
|
||||
def execute(main, description=None, parse_args=None, toggle_input=True, toggle_output=True):
|
||||
if parse_args is None:
|
||||
parse_args = []
|
||||
|
||||
parser = ArgumentParser(
|
||||
description = description,
|
||||
formatter_class = ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
add_io_arguments(parser, toggle_input, toggle_output)
|
||||
|
||||
for a in parse_args:
|
||||
parser.add_argument(*a[0], **a[1])
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
with get_combined_stream(args.input_file, args.output_file) as stream:
|
||||
return main(args, stream)
|
@ -1,16 +1,15 @@
|
||||
from os import getenv as os_getenv
|
||||
|
||||
from py.lib.scwrypts.exceptions import MissingVariableError
|
||||
from py.lib.scwrypts.run import run
|
||||
|
||||
|
||||
def getenv(name, required=True):
|
||||
value = os_getenv(name, None)
|
||||
|
||||
if value == None:
|
||||
run('zsh/scwrypts/environment/stage-variables', name)
|
||||
|
||||
if required and not value:
|
||||
raise MissingVariableError(name)
|
||||
|
||||
if value == '':
|
||||
value = None
|
||||
|
||||
return value
|
||||
|
@ -1,11 +1,22 @@
|
||||
from bpython import embed
|
||||
|
||||
|
||||
def interactive(function):
|
||||
def main(*args, **kwargs):
|
||||
print('preparing interactive environment...')
|
||||
local_vars = function(*args, **kwargs)
|
||||
print('environment ready; user, GO! :)')
|
||||
embed(local_vars)
|
||||
def interactive(variable_descriptions):
|
||||
def outer(function):
|
||||
|
||||
return main
|
||||
def inner(*args, **kwargs):
|
||||
|
||||
print('\npreparing interactive environment...\n')
|
||||
|
||||
local_vars = function(*args, **kwargs)
|
||||
|
||||
print('\n\n'.join([
|
||||
f'>>> {x}' for x in variable_descriptions
|
||||
]))
|
||||
print('\nenvironment ready; user, GO! :)\n')
|
||||
|
||||
embed(local_vars)
|
||||
|
||||
return inner
|
||||
|
||||
return outer
|
||||
|
@ -30,6 +30,9 @@ def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwarg
|
||||
|
||||
yield stdin if is_read else stdout
|
||||
|
||||
if not is_read:
|
||||
stdout.flush()
|
||||
|
||||
|
||||
def add_io_arguments(parser, toggle_input=True, toggle_output=True):
|
||||
if toggle_input:
|
||||
@ -49,3 +52,35 @@ def add_io_arguments(parser, toggle_input=True, toggle_output=True):
|
||||
help = 'path to output file; omit for stdout',
|
||||
required = False,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_combined_stream(input_file=None, output_file=None):
|
||||
with get_stream(input_file, 'r') as input_stream, get_stream(output_file, 'w+') as output_stream:
|
||||
yield CombinedStream(input_stream, output_stream)
|
||||
|
||||
|
||||
class CombinedStream:
|
||||
def __init__(self, input_stream, output_stream):
|
||||
self.input = input_stream
|
||||
self.output = output_stream
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
return self.input.read(*args, **kwargs)
|
||||
|
||||
def readline(self, *args, **kwargs):
|
||||
return self.input.readline(*args, **kwargs)
|
||||
|
||||
def readlines(self, *args, **kwargs):
|
||||
return self.input.readlines(*args, **kwargs)
|
||||
|
||||
def write(self, *args, **kwargs):
|
||||
return self.output.write(*args, **kwargs)
|
||||
|
||||
def writeline(self, line):
|
||||
x = self.output.write(f'{line}\n')
|
||||
self.output.flush()
|
||||
return x
|
||||
|
||||
def writelines(self, *args, **kwargs):
|
||||
return self.output.writelines(*args, **kwargs)
|
@ -7,8 +7,9 @@ def run(scwrypt_name, *args):
|
||||
DEPTH = int(getenv('SUBSCWRYPT', '0'))
|
||||
DEPTH += 1
|
||||
|
||||
SCWRYPTS_EXE = Path(__file__).parents[2] / 'scwrypts'
|
||||
SCWRYPTS_EXE = Path(__file__).parents[3] / 'scwrypts'
|
||||
ARGS = ' '.join([str(x) for x in args])
|
||||
print(f'SUBSCWRYPT={DEPTH} {SCWRYPTS_EXE} {scwrypt_name} -- {ARGS}')
|
||||
|
||||
print(f'\n {"--"*DEPTH} ({DEPTH}) BEGIN SUBSCWRYPT : {Path(scwrypt_name).name}')
|
||||
subprocess_run(
|
||||
|
2
py/lib/twilio/__init__.py
Normal file
2
py/lib/twilio/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from py.lib.twilio.client import get_client
|
||||
from py.lib.twilio.send_sms import send_sms
|
18
py/lib/twilio/client.py
Normal file
18
py/lib/twilio/client.py
Normal file
@ -0,0 +1,18 @@
|
||||
from twilio.rest import Client
|
||||
|
||||
from py.lib.scwrypts import getenv
|
||||
|
||||
CLIENT = None
|
||||
|
||||
def get_client():
|
||||
global CLIENT # pylint: disable=global-statement
|
||||
|
||||
if CLIENT is None:
|
||||
print('loading client')
|
||||
CLIENT = Client(
|
||||
username = getenv('TWILIO__API_KEY'),
|
||||
password = getenv('TWILIO__API_SECRET'),
|
||||
account_sid = getenv('TWILIO__ACCOUNT_SID'),
|
||||
)
|
||||
|
||||
return CLIENT
|
57
py/lib/twilio/send_sms.py
Normal file
57
py/lib/twilio/send_sms.py
Normal file
@ -0,0 +1,57 @@
|
||||
from json import dumps
|
||||
from time import sleep
|
||||
|
||||
from py.lib.twilio.client import get_client
|
||||
|
||||
|
||||
def send_sms(to, from_, body, max_char_count=300, stream=None):
|
||||
'''
|
||||
abstraction for twilio.client.messages.create which will break
|
||||
messages into multi-part SMS rather than throwing an error or
|
||||
requiring the use of MMS data
|
||||
|
||||
@param to messages.create parameter
|
||||
@param from_ messages.create parameter
|
||||
@param body messages.create parameter
|
||||
@param max_char_count 1 ≤ N ≤ 1500 (default 300)
|
||||
@param stream used to report success/failure (optional)
|
||||
|
||||
@return a list of twilio MessageInstance objects
|
||||
'''
|
||||
client = get_client()
|
||||
messages = []
|
||||
|
||||
max_char_count = max(1, min(max_char_count, 1500))
|
||||
|
||||
total_sms_parts = 1 + len(body) // max_char_count
|
||||
contains_multiple_parts = total_sms_parts > 1
|
||||
|
||||
for i in range(0, len(body), max_char_count):
|
||||
msg_body = body[i:i+max_char_count]
|
||||
current_part = 1 + i // max_char_count
|
||||
|
||||
if contains_multiple_parts:
|
||||
msg_body = f'{current_part}/{total_sms_parts}\n{msg_body}'
|
||||
|
||||
message = client.messages.create(
|
||||
to = to,
|
||||
from_ = from_,
|
||||
body = msg_body,
|
||||
)
|
||||
|
||||
messages.append(message)
|
||||
|
||||
if stream is not None:
|
||||
stream.writeline(
|
||||
dumps({
|
||||
'sid': message.sid,
|
||||
'to': to,
|
||||
'from': from_,
|
||||
'body': msg_body,
|
||||
})
|
||||
)
|
||||
|
||||
if contains_multiple_parts:
|
||||
sleep(2 if max_char_count <= 500 else 5)
|
||||
|
||||
return messages
|
@ -1,47 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
from py.lib.http.linear import graphql
|
||||
from py.lib.scwrypts import execute
|
||||
|
||||
from py.lib.data.io import get_stream, add_io_arguments
|
||||
from py.lib.linear import graphql
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
|
||||
parser = ArgumentParser(description = 'comment on an issue in linear.app')
|
||||
def get_query(args):
|
||||
body = f'"""from wrobot:\n```\n{args.message}\n```\n"""'
|
||||
return f'''
|
||||
mutation CommentCreate {{
|
||||
commentCreate(
|
||||
input: {{
|
||||
issueId: "{args.issue_id}"
|
||||
body: {body}
|
||||
}}
|
||||
) {{ success }}
|
||||
}}'''
|
||||
|
||||
parser.add_argument(
|
||||
'-i', '--issue',
|
||||
dest = 'issue_id',
|
||||
help = 'issue short-code (e.g. CLOUD-319)',
|
||||
required = True,
|
||||
def main(args, stream):
|
||||
response = graphql(get_query(args))
|
||||
stream.writeline(response)
|
||||
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'comment on an inssue in linear.app',
|
||||
parse_args = [
|
||||
( ['-d', '--issue-id'], {
|
||||
'dest' : 'issue_id',
|
||||
'help' : 'issue short-code (e.g. CLOUD-319)',
|
||||
'required' : True,
|
||||
}),
|
||||
( ['-m', '--message'], {
|
||||
'dest' : 'message',
|
||||
'help' : 'comment to post to the target issue',
|
||||
'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)
|
||||
|
@ -1,25 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
from argparse import ArgumentParser
|
||||
from py.lib.redis import get_client
|
||||
from py.lib.scwrypts import execute, interactive, getenv
|
||||
|
||||
from py.lib.redis.client import Client
|
||||
from py.lib.scwrypts import interactive, getenv
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise Exception('executable only; must run through scwrypts')
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
|
||||
parser = ArgumentParser(description = 'establishes a redis client in an interactive python shell')
|
||||
args = parser.parse_args()
|
||||
|
||||
@interactive
|
||||
def main():
|
||||
@interactive([
|
||||
f'r = StrictRedis(\'{getenv("REDIS_HOST")}:{getenv("REDIS_PORT")}\')',
|
||||
])
|
||||
def main(_args, _stream):
|
||||
# pylint: disable=possibly-unused-variable
|
||||
r = Client
|
||||
|
||||
print(f'''
|
||||
>>> r = StrictRedis({getenv("REDIS_HOST")}:{getenv("REDIS_PORT")})
|
||||
''')
|
||||
|
||||
r = get_client()
|
||||
return locals()
|
||||
|
||||
main()
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'establishes a redis client in an interactive python shell',
|
||||
parse_args = [],
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
redis
|
||||
bpython
|
||||
pyfzf
|
||||
pyyaml
|
||||
redis
|
||||
twilio
|
||||
|
0
py/twilio/__init__.py
Normal file
0
py/twilio/__init__.py
Normal file
65
py/twilio/send-sms.py
Executable file
65
py/twilio/send-sms.py
Executable file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python
|
||||
from sys import stderr
|
||||
|
||||
from py.lib.scwrypts import execute, getenv
|
||||
from py.lib.twilio import send_sms
|
||||
|
||||
from py.lib.scwrypts.exceptions import ImportedExecutableError, MissingFlagAndEnvironmentVariableError
|
||||
|
||||
if __name__ != '__main__':
|
||||
raise ImportedExecutableError()
|
||||
|
||||
#####################################################################
|
||||
|
||||
def main(args, stream):
|
||||
if args.body is None:
|
||||
print(f'reading input from {stream.input.name}', file=stderr)
|
||||
args.body = ''.join(stream.readlines()).strip()
|
||||
|
||||
if len(args.body) == 0:
|
||||
args.body = 'PING'
|
||||
|
||||
if args.from_ is None:
|
||||
raise MissingFlagAndEnvironmentVariableError(['-f', '--from'], 'TWILIO__DEFAULT_PHONE_FROM')
|
||||
|
||||
if args.to is None:
|
||||
raise MissingFlagAndEnvironmentVariableError(['-t', '--to'], 'TWILIO__DEFAULT_PHONE_TO')
|
||||
|
||||
send_sms(
|
||||
to = args.to,
|
||||
from_ = args.from_,
|
||||
body = args.body,
|
||||
max_char_count = args.max_char_count,
|
||||
stream = stream,
|
||||
)
|
||||
|
||||
|
||||
#####################################################################
|
||||
execute(main,
|
||||
description = 'send a simple SMS through twilio',
|
||||
parse_args = [
|
||||
( ['-t', '--to'], {
|
||||
'dest' : 'to',
|
||||
'help' : 'phone number of the receipient',
|
||||
'required' : False,
|
||||
'default' : getenv('TWILIO__DEFAULT_PHONE_TO', required=False),
|
||||
}),
|
||||
( ['-f', '--from'], {
|
||||
'dest' : 'from_',
|
||||
'help' : 'phone number of the receipient',
|
||||
'required' : False,
|
||||
'default' : getenv('TWILIO__DEFAULT_PHONE_FROM', required=False),
|
||||
}),
|
||||
( ['-b', '--body'], {
|
||||
'dest' : 'body',
|
||||
'help' : 'message body',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['--max-char-count'], {
|
||||
'dest' : 'max_char_count',
|
||||
'help' : 'separate message into parts by character count (1 < N <= 1500)',
|
||||
'required' : False,
|
||||
'default' : 300,
|
||||
}),
|
||||
]
|
||||
)
|
Reference in New Issue
Block a user