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:
@ -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
|
Reference in New Issue
Block a user