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:
2023-02-21 18:44:27 -07:00
parent 7617c938b1
commit 76a746a53e
196 changed files with 3472 additions and 2053 deletions

View File

@ -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 = [],
)

View File

@ -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 = [],
)

View File

@ -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 = [],
)

View File

@ -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 = [],
)

View File

@ -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 = [],
)

View File

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

145
py/directus/get-items.py Executable file
View 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
View File

61
py/discord/post-message.py Executable file
View 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,
}),
]
)

View File

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

View File

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

View File

@ -0,0 +1 @@
import py.lib.data.converter

View File

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

@ -0,0 +1 @@
from py.lib.fzf.client import fzf, fzf_tail, fzf_head

61
py/lib/fzf/client.py Normal file
View 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,
)

View File

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

View File

@ -0,0 +1,2 @@
from py.lib.http.directus.client import *
from py.lib.http.directus.constant import *

View 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]

View 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',
}

View File

@ -0,0 +1,2 @@
from py.lib.http.discord.client import *
from py.lib.http.discord.send_message import *

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

View 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
},
)

View File

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

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

View File

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

View File

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

View File

@ -1 +1 @@
from py.lib.redis.client import get_client

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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 = [],
)

View File

@ -1,3 +1,5 @@
redis
bpython
pyfzf
pyyaml
redis
twilio

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

65
py/twilio/send-sms.py Executable file
View 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,
}),
]
)