Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
7f14edd039 | |||
1b4060dd1c | |||
6aba11d0be | |||
a945daeecc | |||
2ef20860c4 | |||
3fe01a7263 | |||
4146a0d297 | |||
8f3e862086 | |||
aefd575539 | |||
26992d2f01 | |||
487fa65d38 | |||
3ff44f8e58 | |||
f0a6b16a0c | |||
74fe48cc4a | |||
6fe5b8e26a | |||
3d1eb9e03d | |||
bd554f1460 | |||
768bd1444e | |||
4ccb79f1e4 | |||
6b15491066 | |||
8427ad40f0 | |||
1d3eb77235 | |||
406ee85d46 | |||
7709c7e3db | |||
8d3e6ae46f | |||
fec8a7ec94 | |||
534a2011a1 | |||
e1b6c3e4f0 | |||
1adb45d75e | |||
62ab5404cf | |||
e43c07f75a | |||
fc5f80232e | |||
695eea2985 | |||
a739d3b5a2 | |||
a200c1eb22 | |||
f3e70c61cb | |||
72e831da33 | |||
a03885e8db | |||
6cc10e3f4f | |||
4a1208942d | |||
91780024f0 | |||
3ca4fe0c65 | |||
e6dfff255c | |||
15942bb08d | |||
6f42c9cb16 | |||
570fc6a435 | |||
768350e6ab | |||
531aa52146 | |||
f8ccce9285 | |||
6fc17bcfe5 | |||
2034325ac9 | |||
ab567f6950 | |||
e199e9bf91 | |||
4c161aba49 | |||
3ea2e0cd8f | |||
e0cbf58b3c | |||
09c214f939 | |||
e2c6007a65 | |||
620d07f1a8 | |||
4baacd9c32 | |||
6c546ebb6f | |||
9783119a7d | |||
a94d6bc197 | |||
76a746a53e | |||
7617c938b1 | |||
a1256bb0af |
.circleci
.config
.env.template.env.template.descriptions.github/workflows
README.mdaction.yamldocs
global
plugins
ci
kube
.config
README.mddriver
get-contextget-namespacekube.scwrypts.zshkubectl
meta
redis
serveset-contextset-namespacepy
data
__init__.py
convert
directus
discord
hello-world.pyhello_world.pylib
.gitignoreREADME.mdpyproject.toml
scwrypts
__init__.py
data
env.pyfzf
http
io
redis
scwrypts
352
.circleci/config.yml
Normal file
352
.circleci/config.yml
Normal file
@ -0,0 +1,352 @@
|
||||
---
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
python: circleci/python@2.1.1
|
||||
|
||||
|
||||
executors:
|
||||
archlinux:
|
||||
docker:
|
||||
- image: archlinux:base-devel
|
||||
resource_class: small
|
||||
working_directory: /
|
||||
|
||||
python:
|
||||
docker:
|
||||
- image: cimg/python:3.11
|
||||
resource_class: small
|
||||
|
||||
nodejs:
|
||||
docker:
|
||||
- image: node:18
|
||||
resource_class: medium
|
||||
|
||||
zsh:
|
||||
docker:
|
||||
- image: alpine:3
|
||||
resource_class: small
|
||||
|
||||
commands:
|
||||
archlinux-run:
|
||||
description: execute steps in the archlinux container as the CI user
|
||||
parameters:
|
||||
_name:
|
||||
type: string
|
||||
command:
|
||||
type: string
|
||||
working_directory:
|
||||
type: string
|
||||
default: /home/ci
|
||||
steps:
|
||||
- run:
|
||||
name: << parameters._name >>
|
||||
working_directory: << parameters.working_directory >>
|
||||
command: su ci -c '<< parameters.command >>'
|
||||
|
||||
|
||||
custom:
|
||||
archlinux:
|
||||
prepare:
|
||||
- &archlinux-prepare
|
||||
run:
|
||||
name: prepare archlinux dependencies
|
||||
command: |
|
||||
pacman --noconfirm -Syu git openssh ca-certificates-utils
|
||||
useradd -m ci
|
||||
echo "ci ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
|
||||
temp-downgrade-fakeroot:
|
||||
- &archlinux-temp-downgrade-fakeroot
|
||||
run:
|
||||
name: downgrade fakeroot to v1.34 (v1.35 and v1.36 are confirmed to break)
|
||||
command: |
|
||||
pacman -U --noconfirm https://archive.archlinux.org/packages/f/fakeroot/fakeroot-1.34-1-x86_64.pkg.tar.zst
|
||||
|
||||
clone-aur:
|
||||
- &archlinux-clone-aur
|
||||
archlinux-run:
|
||||
_name: clone aur/scwrypts
|
||||
command: git clone https://aur.archlinux.org/scwrypts.git aur
|
||||
|
||||
clone-scwrypts:
|
||||
- &archlinux-clone-scwrypts
|
||||
run:
|
||||
name: clone wrynegade/scwrypts
|
||||
working_directory: /home/ci
|
||||
command: |
|
||||
GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git clone -b "$(echo $CIRCLE_BRANCH | grep . || echo $CIRCLE_TAG)" "$CIRCLE_REPOSITORY_URL" scwrypts
|
||||
chown -R ci:ci ./scwrypts
|
||||
|
||||
|
||||
jobs:
|
||||
require-full-semver:
|
||||
executor: python
|
||||
steps:
|
||||
- run:
|
||||
name: check CIRCLE_TAG for full semantic version
|
||||
command: |
|
||||
: \
|
||||
&& [ $CIRCLE_TAG ] \
|
||||
&& [[ $CIRCLE_TAG =~ ^v[0-9]*.[0-9]*.[0-9]*$ ]] \
|
||||
;
|
||||
|
||||
aur-test:
|
||||
executor: archlinux
|
||||
steps:
|
||||
- *archlinux-prepare
|
||||
- *archlinux-temp-downgrade-fakeroot
|
||||
- *archlinux-clone-aur
|
||||
- *archlinux-clone-scwrypts
|
||||
- archlinux-run:
|
||||
_name: test aur build on current source
|
||||
working_directory: /home/ci/aur
|
||||
command: >-
|
||||
:
|
||||
&& PKGVER=$(sed -n "s/^pkgver=//p" ./PKGBUILD)
|
||||
&& cp -r ../scwrypts ../scwrypts-$PKGVER
|
||||
&& rm -rf ../scwrypts-$PKGVER/.circleci
|
||||
&& rm -rf ../scwrypts-$PKGVER/.git
|
||||
&& rm -rf ../scwrypts-$PKGVER/.gitattributes
|
||||
&& rm -rf ../scwrypts-$PKGVER/.gitignore
|
||||
&& rm -rf ../scwrypts-$PKGVER/.github
|
||||
&& tar -czf scwrypts.tar.gz ../scwrypts-$PKGVER
|
||||
&& echo "source=(scwrypts.tar.gz)" >> PKGBUILD
|
||||
&& echo "sha256sums=(SKIP)" >> PKGBUILD
|
||||
&& makepkg --noconfirm -si
|
||||
&& echo validating scwrypts version
|
||||
&& scwrypts --version | grep "^scwrypts v$PKGVER$"
|
||||
;
|
||||
|
||||
aur-publish:
|
||||
executor: archlinux
|
||||
steps:
|
||||
- *archlinux-prepare
|
||||
- *archlinux-temp-downgrade-fakeroot
|
||||
- *archlinux-clone-aur
|
||||
- archlinux-run:
|
||||
_name: update PKGBUILD and .SRCINFO
|
||||
working_directory: /home/ci/aur
|
||||
command: >-
|
||||
:
|
||||
&& NEW_VERSION=$(echo $CIRCLE_TAG | sed 's/^v//')
|
||||
&& sed "s/pkgver=.*/pkgver=$NEW_VERSION/; s/^pkgrel=.*/pkgrel=1/; /sha256sums/d" PKGBUILD -i
|
||||
&& makepkg -g >> PKGBUILD
|
||||
&& makepkg --printsrcinfo > .SRCINFO
|
||||
;
|
||||
- archlinux-run:
|
||||
_name: sanity check for version build
|
||||
working_directory: /home/ci/aur
|
||||
command: >-
|
||||
:
|
||||
&& makepkg --noconfirm -si
|
||||
&& scwrypts --version
|
||||
&& scwrypts --version | grep -q "^scwrypts $CIRCLE_TAG\$"
|
||||
;
|
||||
- archlinux-run:
|
||||
_name: publish new version
|
||||
working_directory: /home/ci/aur
|
||||
command: >-
|
||||
:
|
||||
&& git add PKGBUILD .SRCINFO
|
||||
&& git -c user.email=yage@yage.io -c user.name=yage commit -am "$CIRCLE_TAG"
|
||||
&& eval $(ssh-agent)
|
||||
&& echo -e $SSH_KEY_PRIVATE__AUR | ssh-add -
|
||||
&& git remote add upstream ssh://aur@aur.archlinux.org/scwrypts.git
|
||||
&& GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" git push upstream
|
||||
;
|
||||
|
||||
nodejs-test:
|
||||
executor: nodejs
|
||||
working_directory: ~/scwrypts/zx/lib
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/scwrypts
|
||||
|
||||
- restore_cache:
|
||||
name: restore pnpm cache
|
||||
keys:
|
||||
- pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
|
||||
|
||||
- run:
|
||||
name: pnpm install
|
||||
command: |
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest-8 --activate
|
||||
pnpm config set store-dir .pnpm-store
|
||||
pnpm install
|
||||
|
||||
- save_cache:
|
||||
name: save pnpm cache
|
||||
key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
|
||||
paths:
|
||||
- .pnpm-store
|
||||
|
||||
- run: pnpm test
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
|
||||
nodejs-publish:
|
||||
executor: nodejs
|
||||
working_directory: ~/scwrypts/zx/lib
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/scwrypts
|
||||
|
||||
- restore_cache:
|
||||
name: restore pnpm cache
|
||||
keys:
|
||||
- pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
|
||||
|
||||
- run:
|
||||
name: pnpm install
|
||||
command: |
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest-8 --activate
|
||||
pnpm config set store-dir .pnpm-store
|
||||
pnpm install
|
||||
|
||||
- save_cache:
|
||||
name: save pnpm cache
|
||||
key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }}
|
||||
paths:
|
||||
- .pnpm-store
|
||||
|
||||
- run:
|
||||
name: publish
|
||||
command: |
|
||||
: \
|
||||
&& [ $CIRCLE_TAG ] \
|
||||
&& pnpm build \
|
||||
&& pnpm version $CIRCLE_TAG \
|
||||
&& pnpm set //registry.npmjs.org/:_authToken=$NPM_TOKEN \
|
||||
&& pnpm publish --no-git-checks \
|
||||
;
|
||||
|
||||
python-test:
|
||||
executor: python
|
||||
working_directory: ~/scwrypts/py/lib
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/scwrypts
|
||||
- run:
|
||||
name: pytest
|
||||
command: |
|
||||
: \
|
||||
&& pip install . .[test] \
|
||||
&& pytest \
|
||||
;
|
||||
- run: pip install build && python -m build
|
||||
|
||||
python-publish:
|
||||
executor: python
|
||||
working_directory: ~/scwrypts/py/lib
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/scwrypts
|
||||
- run: pip install build && python -m build
|
||||
- run: pip install twine && twine upload dist/*
|
||||
|
||||
zsh-test:
|
||||
executor: zsh
|
||||
working_directory: ~/scwrypts
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/scwrypts
|
||||
- run:
|
||||
name: install dependencies
|
||||
command: |
|
||||
: \
|
||||
&& apk add \
|
||||
coreutils \
|
||||
findutils \
|
||||
fzf \
|
||||
perl \
|
||||
sed \
|
||||
gawk \
|
||||
git \
|
||||
jo \
|
||||
jq \
|
||||
util-linux \
|
||||
uuidgen \
|
||||
yq \
|
||||
zsh \
|
||||
;
|
||||
- run:
|
||||
name: scwrypts zsh/unittest
|
||||
command: |
|
||||
~/scwrypts/scwrypts run unittest \
|
||||
;
|
||||
- run:
|
||||
name: scwrypts returns proper success codes
|
||||
command: |
|
||||
~/scwrypts/scwrypts -n sanity check -- --exit-code 0
|
||||
[[ $? -eq 0 ]] || exit 1
|
||||
- run:
|
||||
shell: /bin/sh
|
||||
name: scwrypts returns proper error codes
|
||||
command: |
|
||||
~/scwrypts/scwrypts -n sanity check -- --exit-code 101
|
||||
[[ $? -eq 101 ]] || exit 1
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- aur-test:
|
||||
&dev-filters
|
||||
filters:
|
||||
branches:
|
||||
ignore: /^main$/
|
||||
|
||||
- python-test: *dev-filters
|
||||
- nodejs-test: *dev-filters
|
||||
- zsh-test: *dev-filters
|
||||
|
||||
publish:
|
||||
jobs:
|
||||
- require-full-semver:
|
||||
filters:
|
||||
&only-run-on-full-semver-tag-filters
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+.*$/
|
||||
branches:
|
||||
ignore: /^.*$/
|
||||
|
||||
- aur-test:
|
||||
&only-publish-for-full-semver
|
||||
filters: *only-run-on-full-semver-tag-filters
|
||||
requires:
|
||||
- require-full-semver
|
||||
- aur-publish:
|
||||
#
|
||||
# there's a crazy-low-chance race-condition between this job and the GH Action '../.github/workflows/automatic-release.yaml'
|
||||
# - automatic-release creates the release artifact, but takes no more than 15-30 seconds (current avg:16s max:26s)
|
||||
# - this publish step requires the release artifact, but waits for all language-repository publishes to complete first (a few minutes at least)
|
||||
#
|
||||
# if something goes wrong, this step can be safely rerun after fixing the release artifact :)
|
||||
#
|
||||
filters: *only-run-on-full-semver-tag-filters
|
||||
context: [aur-yage]
|
||||
requires:
|
||||
- aur-test
|
||||
- python-publish
|
||||
- nodejs-publish
|
||||
- zsh-test
|
||||
|
||||
- python-test: *only-publish-for-full-semver
|
||||
- python-publish:
|
||||
filters: *only-run-on-full-semver-tag-filters
|
||||
context: [pypi-yage]
|
||||
requires:
|
||||
- python-test
|
||||
- zsh-test
|
||||
|
||||
- nodejs-test: *only-publish-for-full-semver
|
||||
- nodejs-publish:
|
||||
filters: *only-run-on-full-semver-tag-filters
|
||||
context: [npm-wrynegade]
|
||||
requires:
|
||||
- nodejs-test
|
||||
- zsh-test
|
||||
|
||||
- zsh-test: *only-publish-for-full-semver
|
89
.config/create-new-env
Executable file
89
.config/create-new-env
Executable file
@ -0,0 +1,89 @@
|
||||
#!/bin/zsh
|
||||
#
|
||||
# a temporary template conversion utility for env.template (<=v4)
|
||||
# to env.yaml (>=v5)
|
||||
#
|
||||
eval $(scwrypts --config)
|
||||
use -c scwrypts/environment-files
|
||||
|
||||
ENVIRONMENT_ROOT="$1"
|
||||
[ "$ENVIRONMENT_ROOT" ] || ENVIRONMENT_ROOT="${0:a:h}"
|
||||
|
||||
OLDENV="$ENVIRONMENT_ROOT/env.template"
|
||||
NEWENV="$ENVIRONMENT_ROOT/env.yaml"
|
||||
ENVMAP="$ENVIRONMENT_ROOT/.map.txt"
|
||||
|
||||
GROUP="$2"
|
||||
[ $GROUP ] || GROUP=scwrypts
|
||||
GENERATE_TEMPLATE \
|
||||
| sed '1,4d; /^$/d' \
|
||||
| sed -z 's/# \([^\n]*\)\n\([^\n]*\)=/\2=\n\2=DESCRIPTION=\1/g' \
|
||||
| sed '
|
||||
s/^export //
|
||||
/./i---
|
||||
s/\s\+$//
|
||||
s/__/=/g
|
||||
s/^\(AWS\|REDIS\)_/\1=/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:\n \4:\n \5:/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:\n \4:/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:\n \3:/
|
||||
s/^\([^=]*\)=\([^=]*\)=$/\L\1:\n \2:/
|
||||
s/^\([^=]*\)=$/\L\1:/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3:\n \4:\n \5: \E\6/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3:\n \4: \E\5/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2:\n \3: \E\4/
|
||||
s/^\([^=]*\)=\([^=]*\)=\([^=]\+\)$/\L\1:\n \2: \E\3/
|
||||
s/^\([^=]*\)=\([^=]\+\)$/\L\1: \E\2/
|
||||
s/: (\(.*\))/: [\1]/
|
||||
/^/,/:/{s/_/-/g}
|
||||
' \
|
||||
| sed '
|
||||
s/^ \(description:.*\)/ \1/
|
||||
s/description:/.DESCRIPTION:/
|
||||
' \
|
||||
| sed -z 's/\n\(\s\+\).DESCRIPTION:\([^\n]\+\)/\n\1.DESCRIPTION: >-\n\1 \2/g' \
|
||||
| yq eval-all '. as $item ireduce ({}; . *+ $item)' \
|
||||
> "$NEWENV" \
|
||||
;
|
||||
|
||||
cat -- "$OLDENV" \
|
||||
| sed '
|
||||
s/#.*//
|
||||
/^$/d
|
||||
s/^export //
|
||||
s/\s\+$//
|
||||
s/^\([^=]*\)=.*/\1=\n\1/
|
||||
' \
|
||||
| sed '
|
||||
/^/s/.*/\L&/
|
||||
/^/s/__/./g
|
||||
/^/s/_/-/g
|
||||
s/^/./
|
||||
s/\(aws\|redis\)-/\1./
|
||||
' \
|
||||
| perl -pe 's/=\n/^/' \
|
||||
| column -ts '^' \
|
||||
> "$ENVMAP" \
|
||||
;
|
||||
|
||||
while read line
|
||||
do
|
||||
ENV_VAR=$(echo $line | awk '{print $1;}')
|
||||
LOOKUP=$(echo $line | awk '{print $2;}')
|
||||
|
||||
cp "$NEWENV" "$NEWENV.temp"
|
||||
cat "$NEWENV.temp" \
|
||||
| yq ". | $LOOKUP.[\".ENVIRONMENT\"] = \"$ENV_VAR\"" \
|
||||
| yq 'sort_keys(...)' \
|
||||
> "$NEWENV"
|
||||
;
|
||||
done < "$ENVMAP"
|
||||
|
||||
rm -- "$NEWENV.temp" "$ENVMAP" &>/dev/null
|
||||
|
||||
head -n1 -- "$NEWENV" | grep -q "^{}$" && {
|
||||
echo '---' > "$NEWENV"
|
||||
}
|
||||
|
||||
cat -- "$NEWENV" | yq
|
||||
SUCCESS "new environment saved to '$NEWENV'"
|
25
.config/env.template
Normal file
25
.config/env.template
Normal file
@ -0,0 +1,25 @@
|
||||
#!/bin/zsh
|
||||
export AWS_ACCOUNT=
|
||||
export AWS_PROFILE=
|
||||
export AWS_REGION=
|
||||
export AWS__EFS__LOCAL_MOUNT_POINT=
|
||||
export DIRECTUS__API_TOKEN=
|
||||
export DIRECTUS__BASE_URL=
|
||||
export DISCORD__BOT_TOKEN=
|
||||
export DISCORD__CONTENT_FOOTER=
|
||||
export DISCORD__CONTENT_HEADER=
|
||||
export DISCORD__DEFAULT_AVATAR_URL=
|
||||
export DISCORD__DEFAULT_CHANNEL_ID=
|
||||
export DISCORD__DEFAULT_USERNAME=
|
||||
export DISCORD__DEFAULT_WEBHOOK=
|
||||
export LINEAR__API_TOKEN=
|
||||
export MEDIA_SYNC__S3_BUCKET=
|
||||
export MEDIA_SYNC__TARGETS=
|
||||
export REDIS_AUTH=
|
||||
export REDIS_HOST=
|
||||
export REDIS_PORT=
|
||||
export TWILIO__ACCOUNT_SID=
|
||||
export TWILIO__API_KEY=
|
||||
export TWILIO__API_SECRET=
|
||||
export TWILIO__DEFAULT_PHONE_FROM=
|
||||
export TWILIO__DEFAULT_PHONE_TO=
|
31
.config/env.template.descriptions
Normal file
31
.config/env.template.descriptions
Normal file
@ -0,0 +1,31 @@
|
||||
AWS_ACCOUNT | standard AWS environment variables used by awscli and other tools
|
||||
AWS_PROFILE |
|
||||
AWS_REGION |
|
||||
|
||||
AWS__EFS__LOCAL_MOUNT_POINT | fully-qualified path to mount the EFS drive
|
||||
|
||||
DIRECTUS__API_TOKEN | details for a directus instance
|
||||
DIRECTUS__BASE_URL |
|
||||
|
||||
DISCORD__BOT_TOKEN | details for discord bot
|
||||
DISCORD__CONTENT_HEADER |
|
||||
DISCORD__CONTENT_FOOTER |
|
||||
DISCORD__DEFAULT_AVATAR_URL |
|
||||
DISCORD__DEFAULT_CHANNEL_ID |
|
||||
DISCORD__DEFAULT_USERNAME |
|
||||
DISCORD__DEFAULT_WEBHOOK |
|
||||
|
||||
LINEAR__API_TOKEN | linear.app project management configuration
|
||||
|
||||
MEDIA_SYNC__S3_BUCKET | s3 bucket name and filesystem targets for media backups
|
||||
MEDIA_SYNC__TARGETS |
|
||||
|
||||
REDIS_AUTH | redis connection credentials
|
||||
REDIS_HOST |
|
||||
REDIS_PORT |
|
||||
|
||||
TWILIO__ACCOUNT_SID | twilio account / credentials
|
||||
TWILIO__API_KEY |
|
||||
TWILIO__API_SECRET |
|
||||
TWILIO__DEFAULT_PHONE_FROM |
|
||||
TWILIO__DEFAULT_PHONE_TO |
|
66
.config/env.yaml
Normal file
66
.config/env.yaml
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
aws:
|
||||
.DESCRIPTION: >-
|
||||
standard AWS environment variables used by awscli and other tools
|
||||
account:
|
||||
.ENVIRONMENT: AWS_ACCOUNT
|
||||
efs:
|
||||
local-mount-point:
|
||||
.DESCRIPTION: >-
|
||||
fully-qualified path to mount the EFS drive
|
||||
.ENVIRONMENT: AWS__EFS__LOCAL_MOUNT_POINT
|
||||
profile:
|
||||
.ENVIRONMENT: AWS_PROFILE
|
||||
region:
|
||||
.ENVIRONMENT: AWS_REGION
|
||||
directus:
|
||||
.DESCRIPTION: >-
|
||||
details for a directus instance
|
||||
api-token:
|
||||
.ENVIRONMENT: DIRECTUS__API_TOKEN
|
||||
base-url:
|
||||
.ENVIRONMENT: DIRECTUS__BASE_URL
|
||||
discord:
|
||||
.DESCRIPTION: >-
|
||||
details for discord bot
|
||||
bot-token:
|
||||
.ENVIRONMENT: DISCORD__BOT_TOKEN
|
||||
content-footer:
|
||||
.ENVIRONMENT: DISCORD__CONTENT_FOOTER
|
||||
content-header:
|
||||
.ENVIRONMENT: DISCORD__CONTENT_HEADER
|
||||
default-avatar-url:
|
||||
.ENVIRONMENT: DISCORD__DEFAULT_AVATAR_URL
|
||||
default-channel-id:
|
||||
.ENVIRONMENT: DISCORD__DEFAULT_CHANNEL_ID
|
||||
default-username:
|
||||
.ENVIRONMENT: DISCORD__DEFAULT_USERNAME
|
||||
default-webhook:
|
||||
.ENVIRONMENT: DISCORD__DEFAULT_WEBHOOK
|
||||
linear:
|
||||
.DESCRIPTION: >-
|
||||
linear.app project management configuration
|
||||
api-token:
|
||||
.ENVIRONMENT: LINEAR__API_TOKEN
|
||||
redis:
|
||||
.DESCRIPTION: >-
|
||||
redis connection credentials
|
||||
auth:
|
||||
.ENVIRONMENT: REDIS_AUTH
|
||||
host:
|
||||
.ENVIRONMENT: REDIS_HOST
|
||||
port:
|
||||
.ENVIRONMENT: REDIS_PORT
|
||||
twilio:
|
||||
.DESCRIPTION: >-
|
||||
twilio account / credentials
|
||||
account-sid:
|
||||
.ENVIRONMENT: TWILIO__ACCOUNT_SID
|
||||
api-key:
|
||||
.ENVIRONMENT: TWILIO__API_KEY
|
||||
api-secret:
|
||||
.ENVIRONMENT: TWILIO__API_SECRET
|
||||
default-phone-from:
|
||||
.ENVIRONMENT: TWILIO__DEFAULT_PHONE_FROM
|
||||
default-phone-to:
|
||||
.ENVIRONMENT: TWILIO__DEFAULT_PHONE_TO
|
@ -1,14 +0,0 @@
|
||||
#!/bin/zsh
|
||||
export AWS_ACCOUNT=
|
||||
export AWS_PROFILE=
|
||||
export AWS_REGION=
|
||||
export AWS__EFS__LOCAL_MOUNT_POINT=
|
||||
export AWS__S3__MEDIA_BUCKET=
|
||||
export AWS__S3__MEDIA_TARGETS=
|
||||
export I3__BORDER_PIXEL_SIZE=
|
||||
export I3__DMENU_FONT_SIZE=
|
||||
export I3__GLOBAL_FONT_SIZE=
|
||||
export I3__MODEL_CONFIG=
|
||||
export REDIS_AUTH=
|
||||
export REDIS_HOST=
|
||||
export REDIS_PORT=
|
@ -1,17 +0,0 @@
|
||||
AWS_ACCOUNT | standard AWS environment variables used by awscli and other tools
|
||||
AWS_PROFILE |
|
||||
AWS_REGION |
|
||||
|
||||
AWS__EFS__LOCAL_MOUNT_POINT | fully-qualified path to mount the EFS drive
|
||||
|
||||
AWS__S3__MEDIA_BUCKET | s3 bucket name and filesystem targets for media backups
|
||||
AWS__S3__MEDIA_TARGETS |
|
||||
|
||||
I3__BORDER_PIXEL_SIZE | custom i3 configuration settings
|
||||
I3__DMENU_FONT_SIZE |
|
||||
I3__GLOBAL_FONT_SIZE |
|
||||
I3__MODEL_CONFIG |
|
||||
|
||||
REDIS_AUTH | redis connection credentials
|
||||
REDIS_HOST |
|
||||
REDIS_PORT |
|
19
.github/workflows/automatic-release.yaml
vendored
Normal file
19
.github/workflows/automatic-release.yaml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Automatic Tag-release
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
branches-ignore:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
automatic-tag-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
16
.github/workflows/update-semver.yaml
vendored
Normal file
16
.github/workflows/update-semver.yaml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Update Semver
|
||||
|
||||
on: # yamllint disable-line rule:truthy
|
||||
push:
|
||||
branches-ignore:
|
||||
- '**'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
update-semver:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rickstaa/action-update-semver@v1
|
84
README.md
84
README.md
@ -1,54 +1,76 @@
|
||||
# *Scwrypts* (Wryn + Scripts)
|
||||
# *Scwrypts*
|
||||
|
||||
Scwrypts is a friendly CLI / API for quickly running *sandboxed scripts* in the terminal.
|
||||
Scwrypts is a CLI and API for safely running scripts in the terminal, CI, and other automated environments.
|
||||
|
||||
In modern developer / dev-ops workflows, scripts require a complex configurations.
|
||||
Without a better solution, the developer is cursed to copy lines-upon-lines of variables into terminals, create random text artifacts, or maybe even commit secure credentials into source.
|
||||
Scwrypts leverages ZSH to give hot-key access to run scripts in such environments.
|
||||
Local runs provide a user-friendly approach to quickly execute CI workflows and automations in your terminal.
|
||||
Each local run runs through an interactive, *sandboxed environment* so you never accidentally run dev credentials in production ever again!
|
||||
|
||||
## Major Version Upgrade Notice
|
||||
|
||||
## Dependencies
|
||||
Due to the wide variety of resources used by scripting libraries, the user is expected to manually resolve dependencies.
|
||||
Dependencies are lazy-loaded, and more information can be found by command error messages or in the appropriate README.
|
||||
Please refer to [Version 4 to Version 5 Upgrade Path](./docs/upgrade/v4-to-v5.md) when upgrading from scwrypts v4 to scwrypts v5!
|
||||
|
||||
Because Scwrypts relies on Scwrypts (see [Meta Scwrypts](./zsh/scwrypts)), `zsh` must be installed and [`junegunn/fzf`](https://github.com/junegunn/fzf) must be available on your PATH.
|
||||
## Installation
|
||||
|
||||
## Usage
|
||||
Install Scwrypts by cloning this repository and sourcing `scwrypts.plugin.zsh` in your `zshrc`.
|
||||
You can now run Scwrypts using the ZLE hotkey bound to `SCWRYPTS_SHORTCUT` (default `CTRL + W`).
|
||||
Quick installation is supported through both the [Arch User Repository](https://aur.archlinux.org/packages/scwrypts) and [Homebrew](https://github.com/wrynegade/homebrew-brew/tree/main/Formula)
|
||||
|
||||
```console
|
||||
% cd <path-to-cloned-repo>
|
||||
% echo "source $(pwd)/scwrypts.plugin.zsh >> $HOME/.zshrc"
|
||||
```bash
|
||||
# AUR
|
||||
yay -Syu scwrypts
|
||||
|
||||
# homebrew
|
||||
brew install wrynegade/scwrypts
|
||||
```
|
||||
|
||||
Check out [Meta Scwrypts](./zsh/scwrypts) to quickly set up environments and adjust configuration.
|
||||
### Manual Installation
|
||||
|
||||
|
||||
### No Install / API Usage
|
||||
Alternatively, the `scwrypts` API can be used directly:
|
||||
To install scwrypts manually, clone this repository (and take note of where it is installed)
|
||||
Replacing the `/path/to/cloned-repo` appropriately, add the following line to your `~/.zshrc`:
|
||||
```zsh
|
||||
./scwrypts [--env environment-name] (...script-name-patterns...) [-- ...passthrough arguments... ]
|
||||
source /path/to/cloned-repo/scwrypts.plugin.zsh
|
||||
```
|
||||
|
||||
Given one or more script patterns, Scwrypts will filter the commands by pattern conjunction.
|
||||
If only one command is found which matches the pattern(s), it will immediately begin execution.
|
||||
If multiple commands match, the user will be prompted to select from the filtered list.
|
||||
Of course, if no commands match, Scwrypts will exit with an error.
|
||||
The next time you start your terminal, you can now execute scwrypts by using the plugin shortcut(s) (by default `CTRL + SPACE`).
|
||||
Plugin shortcuts are configurable in your scwrypts configuration file found in `~/.config/scwrypts/config.zsh`, and [here is the default config](./zsh/config.user.zsh).
|
||||
|
||||
Given no script patterns, Scwrypts becomes an interactive CLI, prompting the user to select a command.
|
||||
If you want to use the `scwrypts` program directly, you can either invoke the executable `./scwrypts` or link it in your PATH for easy access.
|
||||
For example, if you have `~/.local/bin` in your PATH, you might run:
|
||||
```zsh
|
||||
ln -s /path/to/cloned-repo/scwrypts "${HOME}/.local/bin/scwrypts"
|
||||
```
|
||||
|
||||
After determining which script to run, if no environment has been specified, Scwrypts prompts the user to choose one.
|
||||
#### PATH Dependencies
|
||||
|
||||
Scwrypts provides a framework for workflows which often depend on a variety of other tools.
|
||||
Although the lazy-loaded dependency model allows hardening in CI and extendability, the user is expected to _resolve required PATH dependencies_.
|
||||
|
||||
When running locally, this is typically as simple as "install the missing program," but this may require additional steps when working in automated environments.
|
||||
|
||||
By default, the `ci` plugin is enabled which provides the `check all dependencies` scwrypt.
|
||||
You can run this to output a comprehensive list of PATH dependencies across all scwrypts groups, but, at a bare minimum, you will need the following applications in your PATH:
|
||||
|
||||
```bash
|
||||
zsh
|
||||
|
||||
grep # GNU
|
||||
sed # GNU
|
||||
sort # GNU
|
||||
|
||||
fzf # https://github.com/junegunn/fzf (only required for interactive / local)
|
||||
jo # https://github.com/jpmens/jo
|
||||
jq # https://github.com/jqlang/jq
|
||||
yq # https://github.com/mikefarah/yq
|
||||
```
|
||||
|
||||
|
||||
### Using in CI/CD or Automated Workflows
|
||||
Set environment variable `CI=true` (and use the no install method) to run in an automated pipeline.
|
||||
## Usage in CI and Automated Environments
|
||||
|
||||
Set environment variable `CI=true` to run scwrypts in an automated environment.
|
||||
There are a few notable changes to this runtime:
|
||||
- **The Scwrypts sandbox environment will not load.** All variables will be read from context.
|
||||
- **The Scwrypts sandbox environment will not load.** All variables will be read directly from the current context.
|
||||
- User yes/no prompts will **always be YES**
|
||||
- Other user input will default to an empty string
|
||||
- Logs will not be captured
|
||||
|
||||
- Logs will not be captured in the user's local cache
|
||||
- In GitHub actions, `*.scwrypts.zsh` groups are detected automatically from the `$GITHUB_WORKSPACE`; set `SCWRYPTS_GITHUB_NO_AUTOLOAD=true` to disable
|
||||
|
||||
## Contributing
|
||||
|
||||
|
69
action.yaml
Normal file
69
action.yaml
Normal file
@ -0,0 +1,69 @@
|
||||
--- # allow running scwrypts in Github Actions
|
||||
name: scwrypts
|
||||
author: yage
|
||||
description: check required dependencies and run a scwrypt
|
||||
|
||||
inputs:
|
||||
scwrypt:
|
||||
description: "args / identifiers for scwrypts CLI (e.g. '--name <scwrypt-name> --group <group-name> --type <type-name>')"
|
||||
required: true
|
||||
|
||||
args:
|
||||
description: "arguments to pass to the scwrypt-to-be-run"
|
||||
required: false
|
||||
|
||||
version:
|
||||
description: "scwrypts version; defaults to latest (minimum v3.7.0)"
|
||||
required: false
|
||||
|
||||
scwrypts-env:
|
||||
description: "override value for SCWRYPTS_ENV"
|
||||
required: false
|
||||
default: "ci.github-actions"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: wrynegade/scwrypts
|
||||
path: ./wrynegade/scwrypts
|
||||
ref: ${{ inputs.version }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: check dependencies
|
||||
shell: bash
|
||||
env:
|
||||
CI: true
|
||||
SCWRYPTS_PLUGIN_ENABLED__ci: 1
|
||||
run: |
|
||||
[ $CI_SCWRYPTS_READY ] && [[ $CI_SCWRYPTS_READY -eq 1 ]] && echo 'setup completed previously' && exit 0
|
||||
|
||||
echo "updating package dependencies"
|
||||
{
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes zsh fzf ripgrep
|
||||
|
||||
for D in $($GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts -n --name check-all-dependencies --group ci --type zsh)
|
||||
do
|
||||
echo "--- installing $D ---"
|
||||
( sudo apt-get install --yes $D; exit 0; )
|
||||
done
|
||||
} > $HOME/.scwrypts.apt-get.log 2>&1
|
||||
|
||||
echo "updating virtual dependencies"
|
||||
$GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts \
|
||||
--name scwrypts/virtualenv/update-all \
|
||||
--group scwrypts \
|
||||
--type zsh \
|
||||
> $HOME/.scwrypts.virtualenv.log 2>&1
|
||||
|
||||
echo "CI_SCWRYPTS_READY=1" >> $GITHUB_ENV
|
||||
exit 0
|
||||
|
||||
- name: run scwrypt
|
||||
shell: bash
|
||||
env:
|
||||
CI: true
|
||||
SCWRYPTS_ENV: ${{ inputs.scwrypts-env }}
|
||||
run: $GITHUB_WORKSPACE/wrynegade/scwrypts/scwrypts ${{inputs.scwrypt}} -- ${{inputs.args}} || exit 1
|
@ -1,6 +1,6 @@
|
||||
___________________________
|
||||
___________________________
|
||||
< ~thanks for your support~ >
|
||||
---------------------------
|
||||
---------------------------
|
||||
\ ^__^
|
||||
\ (oo)\_______
|
||||
(__)\ )\/\
|
||||
|
254
docs/upgrade/v3-to-v4.md
Normal file
254
docs/upgrade/v3-to-v4.md
Normal file
@ -0,0 +1,254 @@
|
||||
# Scwrypts Upgrade v3 to v4 Notes
|
||||
|
||||
Scwrypts v4 brings a big update to the *runstring for `zsh`-type scwrypts*.
|
||||
I've found some of the boilerplate required by each individual script to be confusing and terse, and the goal here is to make it easier and safer to write and run scwrypts in this critical format.
|
||||
|
||||
Jump to [Technical Bits](#technical-bits) if you just want to get started with migration steps.
|
||||
The actual migration _should take less than a minute per script_.
|
||||
This document is deliberately verbose for future reference when I don't remember how any of this works.
|
||||
|
||||
## Ideology and History
|
||||
|
||||
Originally (scwrypts v2 and below) wanted to preserve the direct-use of individual scwrypts.
|
||||
In those versions, executable files could be executed directly (outside of the `scwrypts` function) and still operate with minimal, unwanted consequences.
|
||||
This resulted in a rigid structure which made code-sharing difficult at small scales and untenable in many non-trivial cases.
|
||||
|
||||
Scwrypts v3, begrudgingly introduced a pseudo-import syntax with `use`.
|
||||
This sought to combat the issues of code-sharing and open up the structure of executable scwrypts to the scwrypts-writer.
|
||||
Beyond just clarity, this allowed external libraries to be written and cross-referenced.
|
||||
Although "importing" is an odd (anti?)feature to shell scripting, the way libraries could be written and reused was too helpful and I succumbed to write the `import.driver.zsh` module.
|
||||
|
||||
|
||||
Throughout v3, I tried to maintain the "executability" of individual scwrypts.
|
||||
It's ugly though.
|
||||
Every individual scwrypt relies on `import.driver.zsh` and the context set up by the `scwrypts` executable.
|
||||
While you _could_ run the executable file directly, it would misbehave at-best and fail pretty reliably.
|
||||
|
||||
So... here's v4!
|
||||
Scwrypts v4 accepts the reality that, although `zsh` scwrypts are zsh, they do not stand alone without the proper context setup provided by `scwrypts`.
|
||||
To improve usability, I've abstracted much of the boilerplate so you never have to see it.
|
||||
I've injected safety mechanisms like `--help` arguments and utility mechanisms like flag separation (`-abc` is really `-a -b -c`) into all v4 zsh scwrypts.
|
||||
|
||||
You don't have to worry about checking the context, v4 does that for you!
|
||||
|
||||
You don't have to worry about execution, v4 does that for you!
|
||||
|
||||
So!
|
||||
|
||||
Are you coupling your zsh scripts to `scwrypts` when you write them? Yes.
|
||||
Is that a bad thing? I don't think so.
|
||||
Shell-scripting is such a critical coupler to real-life systems.
|
||||
High-risk-high-impact to SLAs means we cannot allow context mistakes by sysadmins and users.
|
||||
Reusability between local machine, cloud runtime, and CI pipelines is a must.
|
||||
And if you have a need to reign all that in to single, isolated executable files...
|
||||
|
||||
...then good luck <3
|
||||
|
||||
## Technical Bits
|
||||
|
||||
Big idea: let's get rid of v3 boilerplate and make things easy.
|
||||
|
||||
### Your executable must be in a MAIN function
|
||||
|
||||
A main function in shell scripts?
|
||||
Weird!
|
||||
Don't worry, it's easy.
|
||||
|
||||
Take your original scwrypt, and slap the executable stuff into a function called `MAIN` (yes, it must be _exactly_, all-caps `MAIN`):
|
||||
|
||||
```diff
|
||||
#!/usr/bin/env zsh
|
||||
#####################################################################
|
||||
DEPENDENCIES+=(dep-function-a dep-function-b)
|
||||
REQUIRED_ENV+=()
|
||||
|
||||
use do/awesome/stuff --group my-custom-library
|
||||
|
||||
CHECK_ENVIRONMENT
|
||||
#####################################################################
|
||||
|
||||
- echo "do some stuff here"
|
||||
- # ... etc ...
|
||||
- echo.success "completed the stuff"
|
||||
+ MAIN() {
|
||||
+ echo "do some stuff here"
|
||||
+ # ... etc ...
|
||||
+ echo.success "completed the stuff
|
||||
+ }
|
||||
```
|
||||
|
||||
**Don't invoke the function!**
|
||||
Scwrypts will now do that on your behalf.
|
||||
I've already written many scwrypts which _almost_ used this syntax.
|
||||
All I had to do in this case was delete the function invocation at the end:
|
||||
|
||||
```diff
|
||||
# ... top boilerplate ...
|
||||
MAIN() {
|
||||
echo.success "look at me I'm so cool I already wrote this in a main function"
|
||||
}
|
||||
-
|
||||
- #####################################################################
|
||||
- MAIN $@
|
||||
```
|
||||
|
||||
Again, **do not invoke the function**. Just name it `MAIN` and you're good-to-go!
|
||||
|
||||
### Great news!
|
||||
|
||||
Great news!
|
||||
We have finished with *all of the necessary steps* to migrate to v4!
|
||||
Easy!
|
||||
|
||||
While you're here, let's do a couple more things to cleanup your scwrypts (I promise they are also easy and will take less than a few seconds for each)!
|
||||
|
||||
### Remove the boilerplate
|
||||
|
||||
Were you confused by all that garbage at the top?
|
||||
Awesome!
|
||||
Just get rid of any of it you don't use.
|
||||
|
||||
While you _probably_ will still need whatever dependencies you already defined, feel free to get rid of empty config lists like `DEPENDENCIES+=()`.
|
||||
For non-empty lists, the syntax remains the same (use the `+=` and make sure it's an array-type `()` just like before!)
|
||||
|
||||
Also you can ditch the `CHECK_ENVIRONMENT`.
|
||||
While it won't hurt, v4 already does this, so just get rid of it.
|
||||
Here's my recommended formatting:
|
||||
```diff
|
||||
#!/usr/bin/env zsh
|
||||
- #####################################################################
|
||||
DEPENDENCIES+=(dep-function-a dep-function-b)
|
||||
- REQUIRED_ENV+=()
|
||||
|
||||
use do/awesome/stuff --group my-custom-library
|
||||
-
|
||||
- CHECK_ENVIRONMENT
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
echo "do some stuff here"
|
||||
# ... etc ...
|
||||
echo.success "completed the stuff
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Get rid of `--help` argument processing
|
||||
|
||||
Scwrypts v4 injects the `--help` argument into every zsh scwrypt.
|
||||
So there's no need to process it manually anymore!
|
||||
|
||||
We can now eliminate the help case from any MAIN body or library function:
|
||||
|
||||
```diff
|
||||
MAIN() {
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
# ... a bunch of cases ...
|
||||
- -h | --help ) USAGE; return 0 ;;
|
||||
# ... a bunch of cases ...
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
}
|
||||
```
|
||||
|
||||
While you probably weren't doing this, you can also do the same for any logic which splits arguments input like `-abc` which should be read as `-a -b -c`.
|
||||
If you know how to do this, you know how to get rid of it.
|
||||
|
||||
### Write some help docs
|
||||
|
||||
Okay this one might take a little bit of time if you haven't done it already (but this is the last recommended step! hang in there and make your stuff better!).
|
||||
If you _have_ done it already, typically by writing a variable called "USAGE" in your code, maybe consider the _new and improved_ way to write your help strings.
|
||||
|
||||
Returning to our original `MAIN()` example, I'll add some options parsing so we should now look something like this:
|
||||
```sh
|
||||
#!/usr/bin/env zsh
|
||||
DEPENDENCIES+=(dep-function-a dep-function-b)
|
||||
|
||||
use do/awesome/stuff --group my-custom-library
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
local A
|
||||
local B=false
|
||||
local ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
-a | --option-a ) A=$2; shift 1 ;;
|
||||
|
||||
-b | --allow-b ) B=true ;;
|
||||
|
||||
* ) ARGS+=($1) ;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
echo "A : $A\nB : $B\nARGS : $ARGS"
|
||||
}
|
||||
```
|
||||
|
||||
All we have to do is add some usage variables and we're done!
|
||||
I want to call out a few specific ones:
|
||||
- `USAGE__options` provides descriptions for CLI flags like `-a` `--some-flag` and `-a <some value>` (reminder, you *don't* need '-h, --help' anymore!)
|
||||
- `USAGE__args` provides descriptions for non-flag CLI arguments, where order matters (e.g. `cat file-a file-b ... etc`)
|
||||
- `USAGE__description` provides the human-readable description of what your function does
|
||||
- `USAGE__usage` you probably don't need to adjust this one, but it will be everything after the `--` in the usage-line. Defaults to include `[...options...]`, but I suppose you might want to write `USAGE__usage+=' [...args...]` if you 1) have args and 2) are really specific about your help strings.
|
||||
|
||||
Just add another section to define these values before declaring `MAIN`:
|
||||
```sh
|
||||
#!/usr/bin/env zsh
|
||||
DEPENDENCIES+=(dep-function-a dep-function-b)
|
||||
|
||||
use do/awesome/stuff --group my-custom-library
|
||||
#####################################################################
|
||||
|
||||
USAGE__options='
|
||||
-a, --option-a <string> sets the value of the A variable
|
||||
-b, --allow-b enables the B option
|
||||
'
|
||||
|
||||
# remember there's no specific formatting here, just write it nice
|
||||
USAGE__args='
|
||||
N-args All remaining args are inserted into the ARGS variable
|
||||
'
|
||||
|
||||
USAGE__description="
|
||||
This is my cool example function. It's really neato, but does
|
||||
very little.
|
||||
"
|
||||
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
# ... etc ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Now, when we run `scwrypts my sample -- --help`, we get:
|
||||
```txt
|
||||
usage: scwrypts my sample -- [...options...]
|
||||
|
||||
args:
|
||||
N-args All remaining args are inserted into the ARGS variable
|
||||
|
||||
options:
|
||||
-a, --option-a <string> sets the value of the A variable
|
||||
-b, --allow-b enables the B option
|
||||
|
||||
-h, --help display this message and exit
|
||||
|
||||
This is my cool example function. It's really neato, but does
|
||||
very little.
|
||||
```
|
||||
|
||||
### All done
|
||||
|
||||
No more recommendations at this time.
|
||||
Someday I'll have an auto-formatter and a language server to help with go-to-definition, but that's still for the future.
|
||||
|
||||
Thanks for your time and welcome to v4!
|
136
docs/upgrade/v4-to-v5.md
Normal file
136
docs/upgrade/v4-to-v5.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Scwrypts Upgrade v4 to v5 Notes
|
||||
|
||||
Although scwrypts v4 brings a number of new features, most functionality is backwards-compatible.
|
||||
|
||||
## Lots of renames!
|
||||
|
||||
Nearly every module received a rename.
|
||||
This was a decision made to improve both style-consistency and import transparency, but has resulted in a substantial number of breaking changes to `zsh-type scwrypts modules`.
|
||||
|
||||
### `zsh/utils` Functions
|
||||
|
||||
The functions in the underlying library have all been renamed, but otherwise maintain the same functionality.
|
||||
For a full reference, check out the [zsh/utils](../../zsh/utils/utils.module.zsh), but some critical renames are:
|
||||
```bash
|
||||
FZF >> utils.fzf
|
||||
FZF_USER_INPUT >> utils.fzf.user-input
|
||||
LESS >> utils.less
|
||||
YQ >> utils.yq
|
||||
|
||||
SUCCESS >> echo.success
|
||||
ERROR >> echo.error
|
||||
REMINDER >> echo.reminder
|
||||
STATUS >> echo.status
|
||||
WARNING >> echo.warning
|
||||
DEBUG >> echo.debug
|
||||
FAIL >> utils.fail
|
||||
ABORT >> utils.abort
|
||||
|
||||
CHECK_ERRORS >> utils.check-errors
|
||||
|
||||
Yn >> utils.Yn
|
||||
yN >> utils.yN
|
||||
|
||||
EDIT >> utils.io.edit
|
||||
|
||||
CHECK_ENVIRONMENT >> utils.check-environment
|
||||
```
|
||||
|
||||
### `zsh/utils` Color Functions
|
||||
|
||||
Rather than storing ANSI colors as a variable, colors are now stored as a function which prints the color code.
|
||||
Doing this has proven more versatile than trying to extract the value of the variable in several contexts.
|
||||
Rename looks like this for all named ANSI colors:
|
||||
|
||||
```bash
|
||||
$__GREEN >> utils.colors.green
|
||||
$__BRIGHT_RED >> utils.colors.bright-red
|
||||
```
|
||||
|
||||
The most common use case of colors is indirectly through the `echo.*` commands, so a new function now provides _the color used by the associated `echo.*` command_:
|
||||
|
||||
```bash
|
||||
# instead of
|
||||
STATUS "Hello there, ${_BRIGHT_GREEN}bobby${_YELLOW}. How are you?"
|
||||
|
||||
# use
|
||||
echo.status "Hello there, $(utils.colors.bright-green)bobby$(echo.status.color). How are you?
|
||||
```
|
||||
|
||||
### ZSH Scwrypts Module Naming
|
||||
|
||||
**This is the biggest point of refactor.**
|
||||
|
||||
You will notice that modules now declare their functions using a `${scwryptsmodule}` notation.
|
||||
This notation provides a dot-notated name which is intended to provide a consistent, unique naming system in ZSH (remember, everything loaded into the same shell script must have a globally-unique name).
|
||||
Consider the new naming method for the following:
|
||||
|
||||
```bash
|
||||
# v4: zsh/lib/helm/template.module.zsh
|
||||
|
||||
HELM__TEMPLATE__GET() {
|
||||
# ...
|
||||
}
|
||||
|
||||
# v5: zsh/helm/get-template.module.zsh
|
||||
${scwryptsmodule}() {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
Although the import syntax is generally the same, now we reference the full name of the module instead of the arbitrarily defined `HELM__TEMPLATE__GET`:
|
||||
|
||||
```
|
||||
# in some other scwrypt
|
||||
use helm/get-template
|
||||
|
||||
helm.get-template --raw ./my-helm-chart
|
||||
```
|
||||
|
||||
The name `${scwryptsmodule}` is depended on the scwrypts library path.
|
||||
Since there is not an easy way to provide an exhaustive list, go through all the places where you `use` something from the scwrypts core library, and check to see where it is now.
|
||||
One of the critical call-outs is the AWS CLI, which no longer follows the "just use ALL CAPS for function names," but instead is a proper module.
|
||||
|
||||
Both of the following are valid ways to use the scwrypts-safe aws-cli (`AWS` in v4):
|
||||
|
||||
```bash
|
||||
# to import _only_ AWS cli
|
||||
use cloud.aws.cli
|
||||
|
||||
cloud.aws.cli sts get-caller-identity
|
||||
|
||||
# importing the full AWS module also provides an alias
|
||||
use cloud.aws
|
||||
|
||||
cloud.aws sts get-caller-identity
|
||||
```
|
||||
|
||||
### Great news!
|
||||
|
||||
Great news!
|
||||
We have finished with **all of the necessary steps** to migrate to v5!
|
||||
|
||||
If you still have the energy, take some time to make these _recommended_ adjustments too.
|
||||
|
||||
|
||||
### Use the new `${scwryptsmodule}` syntax
|
||||
|
||||
The `${scwryptsmodule}` name is now automatically available in any module.
|
||||
The one change from the `${scwryptsmodule}` in scwrypts core is that **your scwrypts group name is the first dot**.
|
||||
|
||||
If I'm building the scwrypts group called `my-cool-stuff` and open the file `my-cool-stuff/zsh/module-a.module.zsh`, then `${scwryptsmodule}` will refer to `my-cool-stuff.module-a`.
|
||||
|
||||
### Update your `*.scwrypts.zsh` declaration file
|
||||
|
||||
In v4 and earlier, it was tricky to create your own scwrypts group, since you had to create a particular folder structure, and write a `group-name.scwrypts.zsh` file with some somewhat arbitrary requirements.
|
||||
In v5, you can now make any folder a scwrypts group by simply _creating the `*.scwrypts.zsh` file_.
|
||||
|
||||
```bash
|
||||
# this will turn the current folder into the root of a scwrypts group called `my-cool-stuff`
|
||||
touch 'my-cool-stuff.scwrypts.zsh'
|
||||
├── zsh
|
||||
├── zx
|
||||
└── py
|
||||
```
|
||||
|
||||
Advanced options for scwrypts are now [documented in the example](../../scwrypts.scwrypts.zsh), so please refer to it for any additional changes you may need for existing scwrypts modules.
|
@ -1,42 +0,0 @@
|
||||
#####################################################################
|
||||
|
||||
[ ! $SCWRYPTS_ROOT ] && SCWRYPTS_ROOT="$(dirname ${0:a:h})"
|
||||
|
||||
source "${0:a:h}/config.zsh"
|
||||
|
||||
#####################################################################
|
||||
|
||||
__SCWRYPT=1 # arbitrary; indicates scwrypts exists
|
||||
|
||||
__PREFERRED_PYTHON_VERSIONS=(3.10 3.9)
|
||||
__NODE_VERSION=18.0.0
|
||||
|
||||
__ENV_TEMPLATE=$SCWRYPTS_ROOT/.env.template
|
||||
|
||||
#####################################################################
|
||||
|
||||
__GET_PATH_TO_RELATIVE_ARGUMENT() {
|
||||
[[ $1 =~ ^[.] ]] \
|
||||
&& echo $(readlink -f "$EXECUTION_DIR/$1") \
|
||||
|| echo "$1" \
|
||||
;
|
||||
true
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
||||
__RUN_SCWRYPT() {
|
||||
((SUBSCWRYPT+=1))
|
||||
{ printf ' '; printf '--%.0s' {1..$SUBSCWRYPT}; printf " ($SUBSCWRYPT) "; } >&2
|
||||
echo " BEGIN SUBSCWRYPT : $@" >&2
|
||||
|
||||
SUBSCWRYPT=$SUBSCWRYPT SCWRYPTS_ENV=$ENV_NAME \
|
||||
"$SCWRYPTS_ROOT/scwrypts" $@
|
||||
EXIT_CODE=$?
|
||||
|
||||
{ printf ' '; printf '--%.0s' {1..$SUBSCWRYPT}; printf " ($SUBSCWRYPT) "; } >&2
|
||||
echo " END SUBSCWRYPT : $1" >&2
|
||||
((SUBSCWRYPT-=1))
|
||||
|
||||
return $EXIT_CODE
|
||||
}
|
Binary file not shown.
1
plugins/ci/.config/env.template
Normal file
1
plugins/ci/.config/env.template
Normal file
@ -0,0 +1 @@
|
||||
#!/bin/zsh
|
1
plugins/ci/.config/env.yaml
Normal file
1
plugins/ci/.config/env.yaml
Normal file
@ -0,0 +1 @@
|
||||
---
|
3
plugins/ci/README.md
Normal file
3
plugins/ci/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# CI Helper
|
||||
|
||||
Disabled by default, this is used in CI contexts to try and identify missing requirements for the current workflow.
|
33
plugins/ci/check-all-dependencies
Executable file
33
plugins/ci/check-all-dependencies
Executable file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env zsh
|
||||
#####################################################################
|
||||
MAIN() {
|
||||
cd "$(scwrypts.config.group scwrypts root)"
|
||||
|
||||
DEPENDENCIES+=()
|
||||
for group in ${SCWRYPTS_GROUPS[@]}
|
||||
do
|
||||
[[ $group =~ ^ci$ ]] && continue
|
||||
|
||||
GROUP_HOME="$(eval 'echo $SCWRYPTS_ROOT__'$group)"
|
||||
[ $GROUP_HOME ] && [ -d "$GROUP_HOME" ] || continue
|
||||
|
||||
echo.status "checking dependencies for $group"
|
||||
DEPENDENCIES+=($(
|
||||
for file in $(
|
||||
{
|
||||
cd "$GROUP_HOME"
|
||||
rg -l '^DEPENDENCIES\+=\($'
|
||||
rg -l '^DEPENDENCIES\+=\([^)]\+\)'
|
||||
} | grep -v '\.md$' | grep -v 'check-all-dependencies$')
|
||||
do
|
||||
sed -z 's/.*DEPENDENCIES+=(\([^)]*\)).*/\1\n/; s/#.*\n//g; s/\s\+/\n/g' "$GROUP_HOME/$file"
|
||||
done
|
||||
))
|
||||
done
|
||||
|
||||
DEPENDENCIES=(zsh $(echo $DEPENDENCIES | sed 's/ /\n/g' | sort -u | grep '^[-_a-zA-Z]\+$'))
|
||||
|
||||
echo.status "discovered dependencies: ($DEPENDENCIES)"
|
||||
echo $DEPENDENCIES | sed 's/ /\n/g'
|
||||
utils.check-environment && echo.success "all dependencies satisfied"
|
||||
}
|
1
plugins/ci/ci.scwrypts.zsh
Normal file
1
plugins/ci/ci.scwrypts.zsh
Normal file
@ -0,0 +1 @@
|
||||
export ${scwryptsgroup}__type=zsh
|
2
plugins/kube/.config/env.template
Normal file
2
plugins/kube/.config/env.template
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/zsh
|
||||
export SCWRYPTS_KUBECTL_REDIS=
|
1
plugins/kube/.config/env.template.descriptions
Normal file
1
plugins/kube/.config/env.template.descriptions
Normal file
@ -0,0 +1 @@
|
||||
SCWRYPTS_KUBECTL_REDIS | (currently only 'managed') 'managed' or 'custom' redis configuration
|
5
plugins/kube/.config/env.yaml
Normal file
5
plugins/kube/.config/env.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
scwrypts-kubectl-redis:
|
||||
.DESCRIPTION: >-
|
||||
[currently only 'managed'] 'managed' or 'custom' redis configuration
|
||||
.ENVIRONMENT: SCWRYPTS_KUBECTL_REDIS
|
4
plugins/kube/.config/static/redis.zsh
Normal file
4
plugins/kube/.config/static/redis.zsh
Normal file
@ -0,0 +1,4 @@
|
||||
export SCWRYPTS_KUBECTL_REDIS_HOST__managed=127.0.0.1
|
||||
export SCWRYPTS_KUBECTL_REDIS_PORT__managed=26379
|
||||
export SCWRYPTS_KUBECTL_REDIS_AUTH__managed=
|
||||
export SCWRYPTS_KUBECTL_REDIS_KEY_PREFIX__managed=
|
10
plugins/kube/README.md
Normal file
10
plugins/kube/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Kubernetes `kubectl` Helper Plugin
|
||||
|
||||
Leverages a local `redis` application to quickly and easily set an alias `k` for `kubectl --context <some-context> --namespace <some-namespace>`.
|
||||
Much like scwrypts environments, `k` aliases are *only* shared amongst session with the same `SCWRYPTS_ENV` to prevent accidental cross-contamination.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
Enable the plugin in `~/.config/scwrypts/config.zsh` by adding `SCWRYPTS_PLUGIN_ENABLED__KUBECTL=1`.
|
||||
Use `k` as your new `kubectl` and checkout `k --help` and `k meta --help`.
|
47
plugins/kube/driver/kubectl.completion.zsh
Normal file
47
plugins/kube/driver/kubectl.completion.zsh
Normal file
@ -0,0 +1,47 @@
|
||||
#####################################################################
|
||||
command -v compdef >/dev/null 2>&1 || return 0
|
||||
#####################################################################
|
||||
|
||||
for CLI in kubectl helm flux
|
||||
do
|
||||
eval "_${CLI[1]}() {
|
||||
local SUBSESSION=0
|
||||
local SUBSESSION_OFFSET=2
|
||||
echo \${words[2]} | grep -q '^[0-9]\\+$' && SUBSESSION=\${words[2]} && SUBSESSION_OFFSET=3
|
||||
|
||||
local PASSTHROUGH_WORDS=($CLI)
|
||||
[[ \$CURRENT -gt \${SUBSESSION_OFFSET} ]] && echo \${words[\${SUBSESSION_OFFSET}]} | grep -qv '^[0-9]\\+$' && {
|
||||
local KUBECONTEXT=\$(k \$SUBSESSION meta get context)
|
||||
local NAMESPACE=\$(k \$SUBSESSION meta get namespace)
|
||||
|
||||
[ \$KUBECONTEXT ] \
|
||||
&& PASSTHROUGH_WORDS+=($([[ $CLI =~ ^helm$ ]] && echo '--kube-context' || echo '--context') \$KUBECONTEXT) \
|
||||
;
|
||||
[ \$NAMESPACE ] \
|
||||
&& PASSTHROUGH_WORDS+=(--namespace \$NAMESPACE) \
|
||||
;
|
||||
}
|
||||
|
||||
local DELIMIT_COUNT=0
|
||||
local WORD
|
||||
for WORD in \${words[@]:1}
|
||||
do
|
||||
case \$WORD in
|
||||
( [0-9]* ) continue ;;
|
||||
( -- )
|
||||
echo \$words | grep -q 'exec' && ((DELIMIT_COUNT+=1))
|
||||
[[ \$DELIMIT_COUNT -eq 0 ]] && ((DELIMIT_COUNT+=1)) && continue
|
||||
;;
|
||||
esac
|
||||
PASSTHROUGH_WORDS+=(\"\$WORD\")
|
||||
done
|
||||
|
||||
echo \"\$words\" | grep -q '\\s\\+$' && PASSTHROUGH_WORDS+=(' ')
|
||||
|
||||
words=\"\${PASSTHROUGH_WORDS[@]}\"
|
||||
_$CLI
|
||||
}
|
||||
"
|
||||
|
||||
compdef _${CLI[1]} ${CLI[1]}
|
||||
done
|
198
plugins/kube/driver/kubectl.driver.zsh
Normal file
198
plugins/kube/driver/kubectl.driver.zsh
Normal file
@ -0,0 +1,198 @@
|
||||
[[ $SCWRYPTS_KUBECTL_DRIVER_READY -eq 1 ]] && return 0
|
||||
|
||||
unalias k h f >/dev/null 2>&1
|
||||
k() { _SCWRYPTS_KUBECTL_DRIVER kubectl $@; }
|
||||
h() { _SCWRYPTS_KUBECTL_DRIVER helm $@; }
|
||||
f() { _SCWRYPTS_KUBECTL_DRIVER flux $@; }
|
||||
|
||||
_SCWRYPTS_KUBECTL_DRIVER() {
|
||||
[ ! $SCWRYPTS_ENV ] && {
|
||||
echo.error "must set SCWRYPTS_ENV in order to use '$(echo $CLI | head -c1)'"
|
||||
return 1
|
||||
}
|
||||
|
||||
which kube.redis >/dev/null 2>&1 \
|
||||
|| eval "$(scwrypts -n --name meta/get-static-redis-definition --type zsh --group kube)"
|
||||
|
||||
local CLI="$1"; shift 1
|
||||
|
||||
local SCWRYPTS_GROUP CUSTOM_COMMANDS=(meta)
|
||||
for SCWRYPTS_GROUP in ${SCWRYPTS_GROUPS[@]}
|
||||
do
|
||||
CUSTOM_COMMANDS+=($(eval echo '$SCWRYPTS_KUBECTL_CUSTOM_COMMANDS__'$SCWRYPTS_GROUP))
|
||||
done
|
||||
|
||||
##########################################
|
||||
|
||||
local USAGE="
|
||||
usage: - [...args...] [...options...] -- [...$CLI options...]
|
||||
|
||||
args: -
|
||||
|
||||
options: -
|
||||
--subsession [0-9] use indicated subsession (use for script clarity instead of positional arg)
|
||||
|
||||
-h, --help display this help dialogue
|
||||
-v, --verbose output debugging information
|
||||
|
||||
description: -
|
||||
"
|
||||
|
||||
local USAGE__usage=$(echo $CLI | head -c1)
|
||||
|
||||
local USAGE__args="$(
|
||||
{
|
||||
echo "$(utils.colors.print green '[0-9]')^if the first argument is a number 0-9, uses or creates a subsession (default 0)"
|
||||
echo " ^ "
|
||||
for C in ${CUSTOM_COMMANDS[@]}
|
||||
do
|
||||
echo "$(utils.colors.print green ${C})^$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__$C 2>/dev/null)"
|
||||
done
|
||||
} | column -ts '^'
|
||||
)"
|
||||
|
||||
local USAGE__options="
|
||||
-n, --namespace set the namespace
|
||||
-k, --context set the context
|
||||
"
|
||||
|
||||
local USAGE__description="
|
||||
Provides 'k' (kubectl), 'h' (helm), and 'f' (flux) shorthands to the respective
|
||||
utility. These functions leverage redis and scwrypts environments to
|
||||
allow quick selection of contexts and namespaces usable across all
|
||||
active shell instances.
|
||||
|
||||
The scwrypts group 'kubectl' has simple selection executables for
|
||||
kubecontext and namespace, but also provides the library to enable
|
||||
enriched, use-case-sensitive setup of kubernetes context.
|
||||
|
||||
All actions are scoped to the current SCWRYPTS_ENV
|
||||
currently : $(utils.colors.print yellow ${SCWRYPTS_ENV})
|
||||
|
||||
"
|
||||
|
||||
##########################################
|
||||
|
||||
local USER_ARGS=()
|
||||
|
||||
local CUSTOM_COMMAND=0
|
||||
local VERBOSE=0
|
||||
local HELP=0
|
||||
local ERRORS=0
|
||||
|
||||
local COMMAND_SWITCH_CASE="@($(echo $CUSTOM_COMMANDS | sed 's/ /|/g'))"
|
||||
|
||||
[ ! $SUBSESSION ] && local SUBSESSION=0
|
||||
[[ $1 =~ ^[0-9]$ ]] && SUBSESSION=$1 && shift 1
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
meta )
|
||||
CUSTOM_COMMAND=$1
|
||||
SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__$1 ${@:2}
|
||||
break
|
||||
;;
|
||||
|
||||
-v | --verbose ) VERBOSE=1 ;;
|
||||
-h | --help ) HELP=1 ;;
|
||||
|
||||
--subsession ) SUBSESSION=$2; shift 1 ;;
|
||||
|
||||
-n | --namespace )
|
||||
_SCWRYPTS_KUBECTL_DRIVER kubectl meta set namespace $2
|
||||
shift 1
|
||||
;;
|
||||
|
||||
-k | --context | --kube-context )
|
||||
_SCWRYPTS_KUBECTL_DRIVER kubectl meta set context $2
|
||||
shift 1
|
||||
;;
|
||||
|
||||
-- )
|
||||
echo $USER_ARGS | grep -q 'exec' && USER_ARGS+=(--)
|
||||
shift 1
|
||||
break
|
||||
;;
|
||||
|
||||
* )
|
||||
[ ! $CUSTOM_COMMAND ] && {
|
||||
for C in ${CUSTOM_COMMANDS[@]}
|
||||
do
|
||||
[[ $1 =~ ^$C$ ]] && {
|
||||
SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__$1 ${@:2}
|
||||
break
|
||||
}
|
||||
done
|
||||
}
|
||||
USER_ARGS+=($1)
|
||||
;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
while [[ $# -gt 0 ]]; do USER_ARGS+=($1); shift 1; done
|
||||
|
||||
|
||||
utils.check-errors || return 1
|
||||
|
||||
[[ $HELP -eq 1 ]] && { utils.io.usage; return 0; }
|
||||
|
||||
#####################################################################
|
||||
|
||||
local STRICT=$(_SCWRYPTS_KUBECTL_SETTINGS get strict || echo 1)
|
||||
|
||||
case $CUSTOM_COMMAND in
|
||||
0 )
|
||||
local CLI_ARGS=()
|
||||
|
||||
local CONTEXT=$(k meta get context)
|
||||
local NAMESPACE=$(k meta get namespace)
|
||||
|
||||
[ $CONTEXT ] && [[ $CLI =~ ^helm$ ]] && CLI_ARGS+=(--kube-context $CONTEXT)
|
||||
[ $CONTEXT ] && [[ $CLI =~ ^kubectl$ ]] && CLI_ARGS+=(--context $CONTEXT)
|
||||
[ $CONTEXT ] && [[ $CLI =~ ^flux$ ]] && CLI_ARGS+=(--context $CONTEXT)
|
||||
|
||||
[[ $STRICT -eq 1 ]] && {
|
||||
[ $CONTEXT ] || echo.error "missing kubectl 'context'"
|
||||
[ $NAMESPACE ] || echo.error "missing kubectl 'namespace'"
|
||||
|
||||
utils.check-errors --no-fail --no-usage || {
|
||||
echo.error "with 'strict' settings enabled, context and namespace must be set!"
|
||||
echo.reminder "
|
||||
these values can be set directly with
|
||||
$(echo $CLI | head -c1) meta set (namespace|context)
|
||||
"
|
||||
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
[ $NAMESPACE ] && CLI_ARGS+=(--namespace $NAMESPACE)
|
||||
[[ $VERBOSE -eq 1 ]] && {
|
||||
echo.reminder "
|
||||
context '$CONTEXT'
|
||||
namespace '$NAMESPACE'
|
||||
environment '$SCWRYPTS_ENV'
|
||||
subsession '$SUBSESSION'
|
||||
"
|
||||
echo.status "running $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
|
||||
} || {
|
||||
[[ $(_SCWRYPTS_KUBECTL_SETTINGS get context) =~ ^show$ ]] && {
|
||||
echo.reminder "$SCWRYPTS_ENV.$SUBSESSION : $CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}"
|
||||
}
|
||||
}
|
||||
$CLI ${CLI_ARGS[@]} ${USER_ARGS[@]}
|
||||
;;
|
||||
* ) SCWRYPTS_KUBECTL_CUSTOM_COMMAND__$CUSTOM_COMMAND ${USER_ARGS[@]} ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_SCWRYPTS_KUBECTL_SETTINGS() {
|
||||
# (get setting-name) or (set setting-name setting-value)
|
||||
kube.redis h$1 ${SCWRYPTS_ENV}:kubectl:settings ${@:2} | grep .
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
source ${0:a:h}/kubectl.completion.zsh
|
||||
source ${0:a:h}/meta.zsh
|
149
plugins/kube/driver/meta.zsh
Normal file
149
plugins/kube/driver/meta.zsh
Normal file
@ -0,0 +1,149 @@
|
||||
SCWRYPTS_KUBECTL_CUSTOM_COMMAND_PARSE__meta() {
|
||||
USAGE__usage+=" meta"
|
||||
USAGE__args="
|
||||
- get output value of meta variable
|
||||
- set interactively configure value of meta variable
|
||||
- clear clear current subsession variables
|
||||
|
||||
(settings args)
|
||||
- show output context for every command
|
||||
- hide (default) hide output context for every command
|
||||
|
||||
- strict (default) require context *and* namespace to be set
|
||||
- loose do not require context and namespace to be set
|
||||
"
|
||||
USAGE__options=''
|
||||
USAGE__description=$(SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__meta)
|
||||
|
||||
META_SUBARGS="
|
||||
- namespace
|
||||
- context
|
||||
"
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
( -h | --help ) HELP=1 ;;
|
||||
|
||||
( set )
|
||||
USAGE__usage+=" set"
|
||||
USAGE__args="set (namespace|context)"
|
||||
USAGE__description="interactively set a namespace or context for '$SCWRYPTS_ENV'"
|
||||
case $2 in
|
||||
( namespace | context ) USER_ARGS+=($1 $2 $3); [ $3 ] && shift 1 ;;
|
||||
( -h | --help ) HELP=1 ;;
|
||||
( '' )
|
||||
: \
|
||||
&& SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set context \
|
||||
&& SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta set namespace \
|
||||
;
|
||||
return $?
|
||||
;;
|
||||
|
||||
( * ) echo.error "cannot set '$2'" ;;
|
||||
esac
|
||||
shift 1
|
||||
;;
|
||||
|
||||
( get )
|
||||
USAGE__usage+=" get"
|
||||
USAGE__args="get (namespace|context|all)"
|
||||
USAGE__description="output the current namespace or context for '$SCWRYPTS_ENV'"
|
||||
case $2 in
|
||||
( namespace | context | all ) USER_ARGS+=($1 $2) ;;
|
||||
|
||||
( -h | --help ) HELP=1 ;;
|
||||
|
||||
( * ) echo.error "cannot get '$2'" ;;
|
||||
esac
|
||||
shift 1
|
||||
;;
|
||||
|
||||
( copy )
|
||||
USAGE__usage+=" copy"
|
||||
USAGE__args+="copy [0-9]"
|
||||
USAGE__description="copy current subsession ($SUBSESSION) to target subsession id"
|
||||
case $2 in
|
||||
( [0-9] ) USER_ARGS+=($1 $2) ;;
|
||||
( -h | --help ) HELP=1 ;;
|
||||
( * ) echo.error "target session must be a number [0-9]" ;;
|
||||
esac
|
||||
shift 1
|
||||
;;
|
||||
|
||||
( clear | show | hide | strict | loose ) USER_ARGS+=($1) ;;
|
||||
|
||||
( * ) echo.error "no meta command '$1'"
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
}
|
||||
|
||||
SCWRYPTS_KUBECTL_CUSTOM_COMMAND__meta() {
|
||||
case $1 in
|
||||
( get )
|
||||
[[ $2 =~ ^all$ ]] && {
|
||||
local CONTEXT=$(kube.redis get --prefix current:context | grep . || utils.colors.print bright-red "none set")
|
||||
local NAMESPACE=$(kube.redis get --prefix current:namespace | grep . || utils.colors.print bright-red "none set")
|
||||
echo "
|
||||
environment : $SCWRYPTS_ENV
|
||||
context : $CONTEXT
|
||||
namespace : $NAMESPACE
|
||||
|
||||
CLI settings
|
||||
command context : $(_SCWRYPTS_KUBECTL_SETTINGS get context)
|
||||
strict mode : $([[ $STRICT -eq 1 ]] && utils.colors.print green on || utils.colors.print bright-red off)
|
||||
" | sed 's/^ \+//' >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
kube.redis get --prefix current:$2
|
||||
;;
|
||||
|
||||
( set )
|
||||
: \
|
||||
&& scwrypts -n --name set-$2 --type zsh --group kube -- $3 --subsession $SUBSESSION >/dev/null \
|
||||
&& k $SUBSESSION meta get $2 \
|
||||
;
|
||||
;;
|
||||
|
||||
( copy )
|
||||
: \
|
||||
&& echo.status "copying $1 to $2" \
|
||||
&& scwrypts -n --name set-context --type zsh --group kube -- --subsession $2 $(k $1 meta get context | grep . || echo 'reset') \
|
||||
&& scwrypts -n --name set-namespace --type zsh --group kube -- --subsession $2 $(k $1 meta get namespace | grep . || echo 'reset') \
|
||||
&& echo.success "subsession $1 copied to $2" \
|
||||
;
|
||||
;;
|
||||
|
||||
( clear )
|
||||
scwrypts -n --name set-context --type zsh --group kube -- --subsession $SUBSESSION reset >/dev/null \
|
||||
&& echo.success "subsession $SUBSESSION reset to default"
|
||||
;;
|
||||
|
||||
( show )
|
||||
_SCWRYPTS_KUBECTL_SETTINGS set context show >/dev/null \
|
||||
&& echo.success "now showing full command context"
|
||||
;;
|
||||
|
||||
( hide )
|
||||
_SCWRYPTS_KUBECTL_SETTINGS set context hide >/dev/null \
|
||||
&& echo.success "now hiding command context"
|
||||
;;
|
||||
|
||||
( loose )
|
||||
_SCWRYPTS_KUBECTL_SETTINGS set strict 0 >/dev/null \
|
||||
&& echo.warning "now running in 'loose' mode"
|
||||
;;
|
||||
|
||||
( strict )
|
||||
_SCWRYPTS_KUBECTL_SETTINGS set strict 1 >/dev/null \
|
||||
&& echo.success "now running in 'strict' mode"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
SCWRYPTS_KUBECTL_CUSTOM_COMMAND_DESCRIPTION__meta() {
|
||||
[ $CLI ] || CLI='kubectl'
|
||||
echo "operations for $CLI session variables and other CLI settings"
|
||||
}
|
7
plugins/kube/get-context
Executable file
7
plugins/kube/get-context
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env zsh
|
||||
use kubectl --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
kube.kubectl.context.get
|
||||
}
|
7
plugins/kube/get-namespace
Executable file
7
plugins/kube/get-namespace
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env zsh
|
||||
use kubectl --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
kube.kubectl.namespace.get
|
||||
}
|
10
plugins/kube/kube.scwrypts.zsh
Normal file
10
plugins/kube/kube.scwrypts.zsh
Normal file
@ -0,0 +1,10 @@
|
||||
export ${scwryptsgroup}__type=zsh
|
||||
export ${scwryptsgroup}__color=$(utils.colors.red)
|
||||
|
||||
#####################################################################
|
||||
|
||||
SCWRYPTS_STATIC_CONFIG__kubectl+=(
|
||||
"${scwryptsgrouproot}/.config/static/redis.zsh"
|
||||
)
|
||||
|
||||
source "${scwryptsgrouproot}/driver/kubectl.driver.zsh"
|
18
plugins/kube/kubectl/cli.module.zsh
Normal file
18
plugins/kube/kubectl/cli.module.zsh
Normal file
@ -0,0 +1,18 @@
|
||||
#####################################################################
|
||||
|
||||
DEPENDENCIES+=(kubectl)
|
||||
|
||||
use redis --group kube
|
||||
|
||||
#####################################################################
|
||||
|
||||
kube.cli() {
|
||||
local NAMESPACE="$(kube.redis get --prefix "current:namespace")"
|
||||
local CONTEXT="$(kube.kubectl.context.get)"
|
||||
|
||||
local ARGS=()
|
||||
[ "${NAMESPACE}" ] && ARGS+=(--namespace "${NAMESPACE}")
|
||||
[ "${CONTEXT}" ] && ARGS+=(--context "${CONTEXT}")
|
||||
|
||||
kubectl ${ARGS[@]} $@
|
||||
}
|
56
plugins/kube/kubectl/context.module.zsh
Normal file
56
plugins/kube/kubectl/context.module.zsh
Normal file
@ -0,0 +1,56 @@
|
||||
#####################################################################
|
||||
|
||||
use --group kube kubectl/cli
|
||||
use --group kube kubectl/namespace
|
||||
use --group kube redis
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.get() { kube.redis get --prefix "current:context"; }
|
||||
|
||||
${scwryptsmodule}.set() {
|
||||
local CONTEXT=$1
|
||||
[ ! "${CONTEXT}" ] && return 1
|
||||
|
||||
[[ "${CONTEXT}" =~ reset ]] && {
|
||||
: \
|
||||
&& kube.redis del --prefix "current:context" \
|
||||
&& kube.kubectl.namespace.set reset \
|
||||
;
|
||||
return $?
|
||||
}
|
||||
|
||||
: \
|
||||
&& kube.redis set --prefix "current:context" "${CONTEXT}" \
|
||||
&& kube.kubectl.namespace.set reset \
|
||||
;
|
||||
}
|
||||
|
||||
${scwryptsmodule}.select() {
|
||||
case "$(kube.kubectl.context.list | grep -v '^reset$' | wc -l)" in
|
||||
( 0 )
|
||||
echo.error "no contexts available"
|
||||
return 1
|
||||
;;
|
||||
( 1 )
|
||||
kube.kubectl.context.list | tail -n1
|
||||
;;
|
||||
( * )
|
||||
kube.kubectl.context.list | utils.fzf 'select a context'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
${scwryptsmodule}.list() {
|
||||
echo reset
|
||||
local ALL_CONTEXTS="$(kube.cli config get-contexts -o name | sort -u)"
|
||||
|
||||
echo "${ALL_CONTEXTS}" | grep -v '^arn:aws:eks'
|
||||
|
||||
[[ "${AWS_ACCOUNT}" ]] && {
|
||||
echo "${ALL_CONTEXTS}" | grep "^arn:aws:eks:.*:${AWS_ACCOUNT}"
|
||||
true
|
||||
} || {
|
||||
echo "${ALL_CONTEXTS}" | grep '^arn:aws:eks'
|
||||
}
|
||||
}
|
17
plugins/kube/kubectl/kubectl.module.zsh
Normal file
17
plugins/kube/kubectl/kubectl.module.zsh
Normal file
@ -0,0 +1,17 @@
|
||||
#
|
||||
# combines kubectl with redis to both facilitate use of kubectl
|
||||
# between varying contexts/namespaces AND grant persistence between
|
||||
# terminal sessions
|
||||
#
|
||||
|
||||
# redis wrapper for kubectl
|
||||
use --group kube kubectl/cli
|
||||
|
||||
# simplify commands for kubecontexts
|
||||
use --group kube kubectl/context
|
||||
|
||||
# simplify commands for namespaces
|
||||
use --group kube kubectl/namespace
|
||||
|
||||
# local redirect commands for remote kubernetes services
|
||||
use --group kube kubectl/service
|
23
plugins/kube/kubectl/namespace.module.zsh
Normal file
23
plugins/kube/kubectl/namespace.module.zsh
Normal file
@ -0,0 +1,23 @@
|
||||
${scwryptsmodule}.get() { kube.redis get --prefix "current:namespace"; }
|
||||
|
||||
${scwryptsmodule}.set() {
|
||||
local NAMESPACE=$1
|
||||
[ ! "${NAMESPACE}" ] && return 1
|
||||
|
||||
[[ "${NAMESPACE}" =~ reset ]] && {
|
||||
kube.redis del --prefix "current:namespace"
|
||||
return $?
|
||||
}
|
||||
|
||||
kube.redis set --prefix "current:namespace" "${NAMESPACE}"
|
||||
}
|
||||
|
||||
${scwryptsmodule}.select() {
|
||||
kube.kubectl.namespace.list | utils.fzf 'select a namespace'
|
||||
}
|
||||
|
||||
${scwryptsmodule}.list() {
|
||||
echo reset
|
||||
echo default
|
||||
kube.cli get namespaces -o name | sed 's/^namespace\///' | sort | grep -v '^default$'
|
||||
}
|
77
plugins/kube/kubectl/service.module.zsh
Normal file
77
plugins/kube/kubectl/service.module.zsh
Normal file
@ -0,0 +1,77 @@
|
||||
#####################################################################
|
||||
|
||||
use --group kube kubectl/cli
|
||||
use --group kube kubectl/context
|
||||
use --group kube kubectl/namespace
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.serve() {
|
||||
[ "${CONTEXT}" ] || local CONTEXT="$(kube.kubectl.context.get)"
|
||||
[ "${CONTEXT}" ] || echo.error 'must configure a context in which to serve'
|
||||
|
||||
[ "${NAMESPACE}" ] || local NAMESPACE="$(kube.kubectl.namespace.get)"
|
||||
[ "${NAMESPACE}" ] || echo.error 'must configure a namespace in which to serve'
|
||||
|
||||
utils.check-errors --no-usage || return 1
|
||||
|
||||
[ "${SERVICE}" ] && SERVICE="$(kube.kubectl.service.list | jq -c "select (.service == \"${SERVICE}\")" || echo ${SERVICE})"
|
||||
[ "${SERVICE}" ] || local SERVICE="$(kube.kubectl.service.select)"
|
||||
[ "${SERVICE}" ] || echo.error 'must provide or select a service'
|
||||
|
||||
kube.kubectl.service.list | grep -q "^${SERVICE}$"\
|
||||
|| echo.error "no service '${SERVICE}' in '${CONFIG}/${NAMESPACE}'"
|
||||
|
||||
utils.check-errors --no-usage || return 1
|
||||
|
||||
##########################################
|
||||
|
||||
SERVICE_PASSWORD="$(kube.kubectl.service.get-password)"
|
||||
kube.kubectl.service.parse
|
||||
|
||||
echo.reminder "attempting to serve ${NAMESPACE}/${SERVICE_NAME}:${SERVICE_PORT}"
|
||||
[ "${SERVICE_PASSWORD}" ] && echo.reminder "password : ${SERVICE_PASSWORD}"
|
||||
|
||||
kube.cli port-forward "service/${SERVICE_NAME}" "${SERVICE_PORT}"
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
||||
${scwryptsmodule}.select() {
|
||||
[ "${NAMESPACE}" ] || local NAMESPACE="$(kube.kubectl.namespace.get)"
|
||||
[ "${NAMESPACE}" ] || return 1
|
||||
|
||||
local SERVICES="$(kube.kubectl.service.list)"
|
||||
local SELECTED="$({
|
||||
echo "namespace service port"
|
||||
echo ${SERVICES} \
|
||||
| jq -r '.service + " " + .port' \
|
||||
| sed "s/^/${NAMESPACE} /" \
|
||||
;
|
||||
} \
|
||||
| column -t \
|
||||
| utils.fzf 'select a service' --header-lines=1 \
|
||||
| awk '{print $2;}' \
|
||||
)"
|
||||
|
||||
echo "${SERVICES}" | jq -c "select (.service == \"${SELECTED}\")"
|
||||
}
|
||||
|
||||
${scwryptsmodule}.list() {
|
||||
kube.cli get service --no-headers\
|
||||
| awk '{print "{\"service\":\""$1"\",\"ip\":\""$3"\",\"port\":\""$5"\"}"}' \
|
||||
| jq -c 'select (.ip != "None")' \
|
||||
;
|
||||
}
|
||||
|
||||
${scwryptsmodule}.get-password() {
|
||||
[ "${PASSWORD_SECRET}" ] && [ "${PASSWORD_KEY}" ] || return 0
|
||||
|
||||
kube.cli get secret "${PASSWORD_SECRET}" -o jsonpath="{.data.${PASSWORD_KEY}}" \
|
||||
| base64 --decode
|
||||
}
|
||||
|
||||
${scwryptsmodule}.parse() {
|
||||
SERVICE_NAME="$(echo "${SERVICE}" | jq -r .service)"
|
||||
SERVICE_PORT="$(echo "${SERVICE}" | jq -r .port | sed 's|/.*$||')"
|
||||
}
|
7
plugins/kube/meta/get-static-redis-definition
Executable file
7
plugins/kube/meta/get-static-redis-definition
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env zsh
|
||||
use redis --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
echo $(kube.redis --get-static-definition)
|
||||
}
|
94
plugins/kube/redis/redis.module.zsh
Normal file
94
plugins/kube/redis/redis.module.zsh
Normal file
@ -0,0 +1,94 @@
|
||||
#####################################################################
|
||||
|
||||
DEPENDENCIES+=(
|
||||
redis-cli
|
||||
docker
|
||||
)
|
||||
|
||||
REQUIRED_ENV+=()
|
||||
|
||||
utils.environment.check SCWRYPTS_KUBECTL_REDIS --default managed
|
||||
|
||||
#####################################################################
|
||||
|
||||
kube.redis() {
|
||||
[ ! $USAGE ] && local USAGE="
|
||||
usage: [...options...]
|
||||
|
||||
options:
|
||||
--subsession [0-9] use a particular subsession
|
||||
|
||||
-p, --prefix apply dynamic prefix to the next command line argument
|
||||
|
||||
--get-prefix output key prefix for current session+subsession
|
||||
--get-static-definition output the static ZSH function definition for kube.redis
|
||||
|
||||
additional arguments and options are passed through to 'redis-cli'
|
||||
"
|
||||
|
||||
local REDIS_ARGS=() USER_ARGS=()
|
||||
|
||||
[ $SUBSESSION ] || local SUBSESSION=0
|
||||
|
||||
local REDIS_PREFIX=$(eval echo '$SCWRYPTS_KUBECTL_REDIS_KEY_PREFIX__'$SCWRYPTS_KUBECTL_REDIS)
|
||||
[ $REDIS_PREFIX ] && REDIS_PREFIX+=':'
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
( -p | --prefix ) USER_ARGS+=("${REDIS_PREFIX}${SCWRYPTS_ENV}:${SUBSESSION}:$2"); shift 1 ;;
|
||||
|
||||
( --subsession ) SUBSESSION=$2; shift 1 ;;
|
||||
|
||||
( --get-prefix ) echo $REDIS_PREFIX; return 0 ;;
|
||||
( --get-static-definition ) ECHO_STATIC_DEFINITION=1 ;;
|
||||
|
||||
( * ) USER_ARGS+=($1) ;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
local REDIS_HOST=$(eval echo '$SCWRYPTS_KUBECTL_REDIS_HOST__'$SCWRYPTS_KUBECTL_REDIS)
|
||||
local REDIS_PORT=$(eval echo '$SCWRYPTS_KUBECTL_REDIS_PORT__'$SCWRYPTS_KUBECTL_REDIS)
|
||||
local REDIS_AUTH=$(eval echo '$SCWRYPTS_KUBECTL_REDIS_AUTH__'$SCWRYPTS_KUBECTL_REDIS)
|
||||
|
||||
[ $REDIS_HOST ] && REDIS_ARGS+=(-h $REDIS_HOST)
|
||||
[ $REDIS_PORT ] && REDIS_ARGS+=(-p $REDIS_PORT)
|
||||
[ $REDIS_AUTH ] && REDIS_ARGS+=(-a $REDIS_AUTH)
|
||||
|
||||
REDIS_ARGS+=(--raw)
|
||||
|
||||
[[ $ECHO_STATIC_DEFINITION -eq 1 ]] && {
|
||||
echo "kube.redis() {\
|
||||
local USER_ARGS=(); \
|
||||
[ ! \$SUBSESSION ] && local SUBSESSION=0 ;\
|
||||
while [[ \$# -gt 0 ]]; \
|
||||
do \
|
||||
case \$1 in
|
||||
( -p | --prefix ) USER_ARGS+=(\"${REDIS_PREFIX}\${SCWRYPTS_ENV}:\${SUBSESSION}:\$2\"); shift 1 ;; \
|
||||
( * ) USER_ARGS+=(\$1) ;; \
|
||||
esac; \
|
||||
shift 1; \
|
||||
done; \
|
||||
redis-cli ${REDIS_ARGS[@]} \${USER_ARGS[@]}; \
|
||||
}" | sed 's/\s\+/ /g'
|
||||
return 0
|
||||
}
|
||||
|
||||
redis-cli ${REDIS_ARGS[@]} ${USER_ARGS[@]}
|
||||
}
|
||||
|
||||
kube.redis ping 2>/dev/null | grep -qi pong || {
|
||||
RPID=$(docker ps -a | grep scwrypts-kubectl-redis | awk '{print $1;}')
|
||||
[ $RPID ] && echo.status 'refreshing redis instance' && docker rm -f $RPID
|
||||
unset RPID
|
||||
|
||||
docker run \
|
||||
--detach \
|
||||
--name scwrypts-kubectl-redis \
|
||||
--publish $SCWRYPTS_KUBECTL_REDIS_PORT__managed:6379 \
|
||||
redis >/dev/null 2>&1
|
||||
|
||||
echo.status 'awaiting redis connection'
|
||||
until kube.redis ping 2>/dev/null | grep -qi pong; do sleep 0.5; done
|
||||
}
|
49
plugins/kube/serve
Executable file
49
plugins/kube/serve
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env zsh
|
||||
use kubectl --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
local USAGE="
|
||||
usage: [service] [...options...]
|
||||
|
||||
args:
|
||||
service (optional) name of the service to forward locally
|
||||
|
||||
options:
|
||||
--context override context
|
||||
--namespace override namespace
|
||||
--subsession kube.redis subsession (default 0)
|
||||
|
||||
to show a required password on screen, use both:
|
||||
--password-secret Secret resource
|
||||
--password-key key within Secret's 'data'
|
||||
|
||||
-h, --help show this dialogue and exit
|
||||
"
|
||||
local CONTEXT NAMESPACE SERVICE
|
||||
local SUBSESSION=0
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
--context ) CONTEXT=$2; shift 1 ;;
|
||||
--namespace ) NAMESPACE=$2; shift 1 ;;
|
||||
--subsession ) SUBSESSION=$2; shift 1 ;;
|
||||
|
||||
--password-secret ) PASSWORD_SECRET=$2; shift 1 ;;
|
||||
--password-key ) PASSWORD_KEY=$2; shift 1 ;;
|
||||
|
||||
-h | --help ) utils.io.usage; return 0 ;;
|
||||
|
||||
* )
|
||||
[ $SERVICE ] && echo.error "unexpected argument '$2'"
|
||||
SERVICE=$1
|
||||
;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
utils.check-errors --fail
|
||||
|
||||
kube.kubectl.serve
|
||||
}
|
39
plugins/kube/set-context
Executable file
39
plugins/kube/set-context
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env zsh
|
||||
use kubectl --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
local USAGE="
|
||||
usage: [context] [...options...]
|
||||
|
||||
args:
|
||||
context (optional) the full name of the kubeconfig context to set
|
||||
|
||||
options:
|
||||
--subsession kube.redis subsession (default 0)
|
||||
|
||||
-h, --help show this dialogue and exit
|
||||
"
|
||||
local CONTEXT
|
||||
local SUBSESSION=0
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
--subsession ) SUBSESSION=$2; shift 1 ;;
|
||||
|
||||
* )
|
||||
[ $CONTEXT ] && echo.error "unexpected argument '$2'"
|
||||
CONTEXT=$1
|
||||
;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
[ $CONTEXT ] || CONTEXT=$(kube.kubectl.context.select)
|
||||
[ $CONTEXT ] || echo.error 'must provide or select a valid kube context'
|
||||
|
||||
utils.check-errors --fail
|
||||
|
||||
kube.kubectl.context.set $CONTEXT
|
||||
}
|
41
plugins/kube/set-namespace
Executable file
41
plugins/kube/set-namespace
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env zsh
|
||||
use kubectl --group kube
|
||||
#####################################################################
|
||||
|
||||
MAIN() {
|
||||
local USAGE="
|
||||
usage: [namespace] [...options...]
|
||||
|
||||
args:
|
||||
namespace (optional) the full name of the namespace context to set
|
||||
|
||||
options:
|
||||
--subsession kube.redis subsession (default 0)
|
||||
|
||||
-h, --help show this dialogue and exit
|
||||
"
|
||||
local NAMESPACE
|
||||
local SUBSESSION=0
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
case $1 in
|
||||
--subsession ) SUBSESSION=$2; shift 1 ;;
|
||||
|
||||
-h | --help ) utils.io.usage; return 0 ;;
|
||||
|
||||
* )
|
||||
[ $NAMESPACE ] && echo.error "unexpected argument '$2'"
|
||||
NAMESPACE=$1
|
||||
;;
|
||||
esac
|
||||
shift 1
|
||||
done
|
||||
|
||||
[ $NAMESPACE ] || NAMESPACE=$(kube.kubectl.namespace.select)
|
||||
[ $NAMESPACE ] || echo.error 'must provide or select a valid namespace'
|
||||
|
||||
utils.check-errors --fail
|
||||
|
||||
kube.kubectl.namespace.set $NAMESPACE
|
||||
}
|
0
py/data/__init__.py
Normal file
0
py/data/__init__.py
Normal file
0
py/data/convert/__init__.py
Normal file
0
py/data/convert/__init__.py
Normal file
20
py/data/convert/csv-to-json.py
Executable file
20
py/data/convert/csv-to-json.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert csv into json'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'csv',
|
||||
output_stream = stream.output,
|
||||
output_type = 'json',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
20
py/data/convert/csv-to-yaml.py
Executable file
20
py/data/convert/csv-to-yaml.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert csv into yaml'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'csv',
|
||||
output_stream = stream.output,
|
||||
output_type = 'yaml',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
20
py/data/convert/json-to-csv.py
Executable file
20
py/data/convert/json-to-csv.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert json into csv'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'json',
|
||||
output_stream = stream.output,
|
||||
output_type = 'csv',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
20
py/data/convert/json-to-yaml.py
Executable file
20
py/data/convert/json-to-yaml.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert json into yaml'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'json',
|
||||
output_stream = stream.output,
|
||||
output_type = 'yaml',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
20
py/data/convert/yaml-to-csv.py
Executable file
20
py/data/convert/yaml-to-csv.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert yaml into csv'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'yaml',
|
||||
output_stream = stream.output,
|
||||
output_type = 'csv',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
20
py/data/convert/yaml-to-json.py
Executable file
20
py/data/convert/yaml-to-json.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from scwrypts.data import convert
|
||||
|
||||
|
||||
description = 'convert yaml into json'
|
||||
parse_args = []
|
||||
|
||||
def main(_args, stream):
|
||||
return convert(
|
||||
input_stream = stream.input,
|
||||
input_type = 'yaml',
|
||||
output_stream = stream.output,
|
||||
output_type = 'json',
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
0
py/directus/__init__.py
Normal file
0
py/directus/__init__.py
Normal file
140
py/directus/get-items.py
Executable file
140
py/directus/get-items.py
Executable file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from json import dumps
|
||||
|
||||
from scwrypts.fzf import fzf, fzf_tail
|
||||
from scwrypts.http import directus
|
||||
|
||||
|
||||
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,
|
||||
}),
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
0
py/discord/__init__.py
Normal file
0
py/discord/__init__.py
Normal file
57
py/discord/post-message.py
Executable file
57
py/discord/post-message.py
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
from json import dumps
|
||||
from sys import stderr
|
||||
|
||||
from scwrypts.http import discord
|
||||
|
||||
|
||||
description = 'post a message to the indicated discord channel'
|
||||
parse_args = [
|
||||
( ['-b', '--body'], {
|
||||
'dest' : 'content',
|
||||
'help' : 'message body',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['-c', '--channel-id'], {
|
||||
'dest' : 'channel_id',
|
||||
'help' : 'override default target channel id',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['-w', '--webhook'], {
|
||||
'dest' : 'webhook',
|
||||
'help' : 'override default target webhook (takes precedence over -c)',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['--avatar-url'], {
|
||||
'dest' : 'avatar_url',
|
||||
'help' : 'override default avatar_url',
|
||||
'required' : False,
|
||||
}),
|
||||
( ['--username'], {
|
||||
'dest' : 'username',
|
||||
'help' : 'override default username',
|
||||
'required' : False,
|
||||
}),
|
||||
]
|
||||
|
||||
def main(args, stream):
|
||||
if args.content is None:
|
||||
print(f'reading input from {stream.input.name}', file=stderr)
|
||||
args.content = ''.join(stream.readlines()).strip()
|
||||
|
||||
if len(args.content) == 0:
|
||||
args.content = 'PING'
|
||||
|
||||
response = discord.send_message(**vars(args))
|
||||
|
||||
stream.writeline(dumps({
|
||||
**(response.json() if response.text != '' else {'message': 'OK'}),
|
||||
'scwrypts_metadata': {},
|
||||
}))
|
||||
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
21
py/hello-world.py
Executable file
21
py/hello-world.py
Executable file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
from scwrypts import execute
|
||||
#####################################################################
|
||||
|
||||
description = 'a simple "Hello, World!" program'
|
||||
parse_args = [
|
||||
( ['-m', '--message'], {
|
||||
'dest' : 'message',
|
||||
'default' : 'HELLO WORLD',
|
||||
'help' : 'message to print',
|
||||
'required' : False,
|
||||
}),
|
||||
]
|
||||
|
||||
def main(args, stream):
|
||||
stream.writeline(args.message)
|
||||
|
||||
|
||||
#####################################################################
|
||||
if __name__ == '__main__':
|
||||
execute(main, description, parse_args)
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
def main():
|
||||
print('HELLO WORLD')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
4
py/lib/.gitignore
vendored
Normal file
4
py/lib/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
dist/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
3
py/lib/README.md
Normal file
3
py/lib/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Python Scwrypts
|
||||
[](https://python.org)
|
||||
<br>
|
59
py/lib/pyproject.toml
Normal file
59
py/lib/pyproject.toml
Normal file
@ -0,0 +1,59 @@
|
||||
[project]
|
||||
name = 'scwrypts'
|
||||
description = 'scwrypts library and invoker'
|
||||
license = 'GPL-3.0-or-later'
|
||||
|
||||
readme = 'README.md'
|
||||
requires-python = '>=3.10'
|
||||
|
||||
authors = [
|
||||
{ name='yage', email='yage@yage.io' },
|
||||
]
|
||||
|
||||
|
||||
classifiers = [
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
]
|
||||
|
||||
dynamic = ['version']
|
||||
|
||||
dependencies = [
|
||||
'bpython',
|
||||
'pyfzf',
|
||||
'pyyaml',
|
||||
'redis',
|
||||
'twilio',
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
'pylint',
|
||||
]
|
||||
|
||||
test = [
|
||||
'pytest',
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = 'https://github.com/wrynegade/scwrypts'
|
||||
issues = 'https://github.com/wrynegade/scwrypts/issues'
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
'hatchling',
|
||||
'versioningit',
|
||||
]
|
||||
build-backend = 'hatchling.build'
|
||||
|
||||
[tool.hatch.version]
|
||||
source = 'versioningit'
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ['./']
|
||||
|
||||
[tool.versioningit.vcs]
|
||||
match = ['v[0-9]*.[0-9]*.[0-9]*']
|
13
py/lib/scwrypts/__init__.py
Normal file
13
py/lib/scwrypts/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
'''
|
||||
scwrypts
|
||||
|
||||
python library functions and invoker for scwrypts
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'scwrypts',
|
||||
'execute',
|
||||
'interactive',
|
||||
]
|
||||
|
||||
from .scwrypts import scwrypts, execute, interactive
|
1
py/lib/scwrypts/data/__init__.py
Normal file
1
py/lib/scwrypts/data/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .converter import convert
|
70
py/lib/scwrypts/data/converter.py
Normal file
70
py/lib/scwrypts/data/converter.py
Normal file
@ -0,0 +1,70 @@
|
||||
import csv
|
||||
import json
|
||||
import yaml
|
||||
|
||||
|
||||
def convert(input_stream, input_type, output_stream, output_type):
|
||||
data = convert_input(input_stream, input_type)
|
||||
write_output(output_stream, output_type, data)
|
||||
|
||||
|
||||
def convert_input(stream, input_type):
|
||||
supported_input_types = {'csv', 'json', 'yaml'}
|
||||
|
||||
if input_type not in supported_input_types:
|
||||
raise ValueError(f'input_type "{input_type}" not supported; must be one of {supported_input_types}')
|
||||
|
||||
return {
|
||||
'csv': _read_csv,
|
||||
'json': _read_json,
|
||||
'yaml': _read_yaml,
|
||||
}[input_type](stream)
|
||||
|
||||
|
||||
def write_output(stream, output_type, data):
|
||||
supported_output_types = {'csv', 'json', 'yaml'}
|
||||
|
||||
if output_type not in supported_output_types:
|
||||
raise ValueError(f'output_type "{output_type}" not supported; must be one of {supported_output_types}')
|
||||
|
||||
return {
|
||||
'csv': _write_csv,
|
||||
'json': _write_json,
|
||||
'yaml': _write_yaml,
|
||||
}[output_type](stream, data)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
def _read_csv(stream):
|
||||
return [dict(line) for line in csv.DictReader(stream)]
|
||||
|
||||
def _write_csv(stream, data):
|
||||
writer = csv.DictWriter(stream, fieldnames=list({
|
||||
key
|
||||
for dictionary in data
|
||||
for key in dictionary.keys()
|
||||
}))
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for value in data:
|
||||
writer.writerow(value)
|
||||
|
||||
#####################################################################
|
||||
|
||||
def _read_json(stream):
|
||||
data = json.loads(stream.read())
|
||||
return data if isinstance(data, list) else [data]
|
||||
|
||||
def _write_json(stream, data):
|
||||
stream.write(json.dumps(data))
|
||||
|
||||
#####################################################################
|
||||
|
||||
def _read_yaml(stream):
|
||||
data = yaml.safe_load(stream)
|
||||
return data if isinstance(data, list) else [data]
|
||||
|
||||
def _write_yaml(stream, data):
|
||||
yaml.dump(data, stream, default_flow_style=False)
|
73
py/lib/scwrypts/data/test_converter.py
Normal file
73
py/lib/scwrypts/data/test_converter.py
Normal file
@ -0,0 +1,73 @@
|
||||
from io import StringIO
|
||||
|
||||
from pytest import raises
|
||||
|
||||
from scwrypts.test import generate
|
||||
|
||||
from .converter import convert
|
||||
|
||||
GENERATE_OPTIONS = {
|
||||
'depth': 1,
|
||||
'minimum': -999999,
|
||||
'maximum': 999999,
|
||||
'dict_key_types': {str, int},
|
||||
'csv_columns_minimum': 10,
|
||||
'csv_columns_maximum': 64,
|
||||
'csv_rows_minimum': 10,
|
||||
'csv_rows_maximum': 64,
|
||||
}
|
||||
|
||||
INPUT_TYPES = {'csv', 'json', 'yaml'}
|
||||
OUTPUT_TYPES = {'csv', 'json', 'yaml'}
|
||||
|
||||
|
||||
def test_convert_to_csv():
|
||||
for input_type in INPUT_TYPES:
|
||||
input_stream = generate(input_type, {
|
||||
**GENERATE_OPTIONS,
|
||||
'data_types': {bool,int,float,str},
|
||||
})
|
||||
|
||||
if isinstance(input_stream, str):
|
||||
input_stream = StringIO(input_stream)
|
||||
|
||||
|
||||
convert(input_stream, input_type, StringIO(), 'csv')
|
||||
|
||||
def test_convert_to_json():
|
||||
for input_type in INPUT_TYPES:
|
||||
input_stream = generate(input_type, GENERATE_OPTIONS)
|
||||
|
||||
if isinstance(input_stream, str):
|
||||
input_stream = StringIO(input_stream)
|
||||
|
||||
convert(input_stream, input_type, StringIO(), 'json')
|
||||
|
||||
def test_convert_to_yaml():
|
||||
for input_type in INPUT_TYPES:
|
||||
input_stream = generate(input_type, GENERATE_OPTIONS)
|
||||
|
||||
if isinstance(input_stream, str):
|
||||
input_stream = StringIO(input_stream)
|
||||
|
||||
convert(input_stream, input_type, StringIO(), 'yaml')
|
||||
|
||||
|
||||
def test_convert_deep_json_to_yaml():
|
||||
input_stream = generate('json', {**GENERATE_OPTIONS, 'depth': 4})
|
||||
convert(input_stream, 'json', StringIO(), 'yaml')
|
||||
|
||||
def test_convert_deep_yaml_to_json():
|
||||
input_stream = generate('yaml', {**GENERATE_OPTIONS, 'depth': 4})
|
||||
convert(input_stream, 'yaml', StringIO(), 'json')
|
||||
|
||||
|
||||
def test_convert_output_unsupported():
|
||||
for input_type in list(INPUT_TYPES):
|
||||
with raises(ValueError):
|
||||
convert(StringIO(), input_type, StringIO(), generate(str))
|
||||
|
||||
def test_convert_input_unsupported():
|
||||
for output_type in list(OUTPUT_TYPES):
|
||||
with raises(ValueError):
|
||||
convert(StringIO(), generate(str), StringIO(), output_type)
|
32
py/lib/scwrypts/env.py
Normal file
32
py/lib/scwrypts/env.py
Normal file
@ -0,0 +1,32 @@
|
||||
from json import loads
|
||||
|
||||
from .scwrypts import scwrypts
|
||||
|
||||
from .scwrypts.exceptions import MissingVariableError
|
||||
|
||||
ENV = {}
|
||||
|
||||
def getenv(name, required=True, default=None):
|
||||
if ENV.get('configuration') is None or ENV.get('environment') is None:
|
||||
full_environment = loads(
|
||||
scwrypts(
|
||||
name = 'scwrypts/environment/getenv',
|
||||
group = 'scwrypts',
|
||||
_type = 'zsh',
|
||||
executable_args = '-n',
|
||||
args = '--all',
|
||||
).stdout
|
||||
)
|
||||
|
||||
ENV['configuration'] = full_environment['configuration']
|
||||
ENV['environment'] = full_environment['environment']
|
||||
|
||||
value = ENV.get('environment', {}).get(name, default)
|
||||
|
||||
if required and not value:
|
||||
raise MissingVariableError(name)
|
||||
|
||||
if value == '':
|
||||
value = None
|
||||
|
||||
return value
|
1
py/lib/scwrypts/fzf/__init__.py
Normal file
1
py/lib/scwrypts/fzf/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .client import fzf, fzf_tail, fzf_head
|
61
py/lib/scwrypts/fzf/client.py
Normal file
61
py/lib/scwrypts/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
py/lib/scwrypts/http/__init__.py
Normal file
1
py/lib/scwrypts/http/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .client import get_request_client
|
25
py/lib/scwrypts/http/client.py
Normal file
25
py/lib/scwrypts/http/client.py
Normal file
@ -0,0 +1,25 @@
|
||||
from requests import request
|
||||
|
||||
|
||||
CLIENTS = {}
|
||||
|
||||
def get_request_client(base_url, headers=None):
|
||||
if CLIENTS.get(base_url, None) is None:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
CLIENTS[base_url] = lambda method, endpoint, **kwargs: request(
|
||||
method = method,
|
||||
url = f'{base_url}/{endpoint}',
|
||||
headers = {
|
||||
**headers,
|
||||
**kwargs.get('headers', {}),
|
||||
},
|
||||
**{
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key != 'headers'
|
||||
},
|
||||
)
|
||||
|
||||
return CLIENTS[base_url]
|
40
py/lib/scwrypts/http/conftest.py
Normal file
40
py/lib/scwrypts/http/conftest.py
Normal file
@ -0,0 +1,40 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from scwrypts.test import get_generator
|
||||
from scwrypts.test.character_set import uri
|
||||
|
||||
generate = get_generator({
|
||||
'str_length_minimum': 8,
|
||||
'str_length_maximum': 128,
|
||||
'uuid_output_type': str,
|
||||
})
|
||||
|
||||
def get_request_client_sample_data():
|
||||
return {
|
||||
'base_url' : generate(str, {'character_set': uri}),
|
||||
'endpoint' : generate(str, {'character_set': uri}),
|
||||
'method' : generate(str),
|
||||
'response' : generate('requests_Response', {'depth': 4}),
|
||||
'payload' : generate(dict, {
|
||||
'depth': 1,
|
||||
'data_types': { str, 'uuid' },
|
||||
}),
|
||||
}
|
||||
|
||||
@fixture(name='sample')
|
||||
def fixture_sample():
|
||||
return SimpleNamespace(
|
||||
**get_request_client_sample_data(),
|
||||
|
||||
headers = generate(dict, {
|
||||
'depth': 1,
|
||||
'data_types': { str, 'uuid' },
|
||||
}),
|
||||
|
||||
payload_headers = generate(dict, {
|
||||
'depth': 1,
|
||||
'data_types': { str, 'uuid' },
|
||||
}),
|
||||
)
|
45
py/lib/scwrypts/http/directus/__init__.py
Normal file
45
py/lib/scwrypts/http/directus/__init__.py
Normal file
@ -0,0 +1,45 @@
|
||||
'''
|
||||
basic scwrypts.http client for directus
|
||||
|
||||
configured by setting DIRECTUS__BASE_URL and DIRECTUS__API_TOKEN in
|
||||
scwrypts environment
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'request',
|
||||
'graphql',
|
||||
'get_collections',
|
||||
'get_fields',
|
||||
'FILTER_OPERATORS',
|
||||
]
|
||||
|
||||
from .client import request
|
||||
from .graphql import graphql
|
||||
from .collections import get_collections
|
||||
from .fields import get_fields
|
||||
|
||||
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',
|
||||
}
|
12
py/lib/scwrypts/http/directus/client.py
Normal file
12
py/lib/scwrypts/http/directus/client.py
Normal file
@ -0,0 +1,12 @@
|
||||
from scwrypts.env import getenv
|
||||
|
||||
from .. import get_request_client
|
||||
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
return get_request_client(
|
||||
base_url = getenv("DIRECTUS__BASE_URL"),
|
||||
headers = {
|
||||
'Authorization': f'bearer {getenv("DIRECTUS__API_TOKEN")}',
|
||||
}
|
||||
)(method, endpoint, **kwargs)
|
18
py/lib/scwrypts/http/directus/collections.py
Normal file
18
py/lib/scwrypts/http/directus/collections.py
Normal file
@ -0,0 +1,18 @@
|
||||
from .client import request
|
||||
|
||||
|
||||
COLLECTIONS = None
|
||||
|
||||
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
|
15
py/lib/scwrypts/http/directus/conftest.py
Normal file
15
py/lib/scwrypts/http/directus/conftest.py
Normal file
@ -0,0 +1,15 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from scwrypts.test.character_set import uri
|
||||
from ..conftest import generate, get_request_client_sample_data
|
||||
|
||||
|
||||
@fixture(name='sample')
|
||||
def fixture_sample():
|
||||
return SimpleNamespace(
|
||||
**get_request_client_sample_data(),
|
||||
api_token = generate(str, {'character_set': uri}),
|
||||
query = generate(str),
|
||||
)
|
16
py/lib/scwrypts/http/directus/fields.py
Normal file
16
py/lib/scwrypts/http/directus/fields.py
Normal file
@ -0,0 +1,16 @@
|
||||
from .client import request
|
||||
|
||||
|
||||
FIELDS = {}
|
||||
|
||||
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]
|
9
py/lib/scwrypts/http/directus/graphql.py
Normal file
9
py/lib/scwrypts/http/directus/graphql.py
Normal file
@ -0,0 +1,9 @@
|
||||
from .client import request
|
||||
|
||||
|
||||
def graphql(query, system=False):
|
||||
return request(
|
||||
'POST',
|
||||
'graphql' if system is False else 'graphql/system',
|
||||
json={'query': query},
|
||||
)
|
43
py/lib/scwrypts/http/directus/test_client.py
Normal file
43
py/lib/scwrypts/http/directus/test_client.py
Normal file
@ -0,0 +1,43 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .client import request
|
||||
|
||||
|
||||
def test_directus_request(sample, _response):
|
||||
assert _response == sample.response
|
||||
|
||||
def test_directus_request_client_setup(sample, _response, mock_get_request_client):
|
||||
mock_get_request_client.assert_called_once_with(
|
||||
base_url = sample.base_url,
|
||||
headers = { 'Authorization': f'bearer {sample.api_token}' },
|
||||
)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response')
|
||||
def fixture_response(sample):
|
||||
return request(
|
||||
method = sample.method,
|
||||
endpoint = sample.endpoint,
|
||||
**sample.payload,
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='mock_getenv', autouse=True)
|
||||
def fixture_mock_getenv(sample):
|
||||
with patch('scwrypts.http.directus.client.getenv',) as mock:
|
||||
mock.side_effect = lambda name: {
|
||||
'DIRECTUS__BASE_URL': sample.base_url,
|
||||
'DIRECTUS__API_TOKEN': sample.api_token,
|
||||
}[name]
|
||||
yield mock
|
||||
|
||||
@fixture(name='mock_get_request_client', autouse=True)
|
||||
def fixture_mock_get_request_client(sample):
|
||||
with patch('scwrypts.http.directus.client.get_request_client') as mock:
|
||||
mock.return_value = lambda method, endpoint, **kwargs: sample.response
|
||||
yield mock
|
45
py/lib/scwrypts/http/directus/test_graphql.py
Normal file
45
py/lib/scwrypts/http/directus/test_graphql.py
Normal file
@ -0,0 +1,45 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .graphql import graphql
|
||||
|
||||
|
||||
def test_directus_graphql(sample, _response, _mock_request):
|
||||
assert _response == sample.response
|
||||
|
||||
def test_directus_graphql_request_payload(sample, _response, _mock_request):
|
||||
_mock_request.assert_called_once_with(
|
||||
'POST',
|
||||
'graphql',
|
||||
json = {'query': sample.query},
|
||||
)
|
||||
|
||||
def test_directus_graphql_system(sample, _response_system):
|
||||
assert _response_system == sample.response
|
||||
|
||||
def test_directus_graphql_system_request_payload(sample, _response_system, _mock_request):
|
||||
_mock_request.assert_called_once_with(
|
||||
'POST',
|
||||
'graphql/system',
|
||||
json = {'query': sample.query},
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response')
|
||||
def fixture_response(sample, _mock_request):
|
||||
return graphql(sample.query)
|
||||
|
||||
|
||||
@fixture(name='_response_system')
|
||||
def fixture_response_system(sample, _mock_request):
|
||||
return graphql(sample.query, system=True)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_mock_request')
|
||||
def fixture_mock_request(sample):
|
||||
with patch('scwrypts.http.directus.graphql.request') as mock:
|
||||
mock.return_value = sample.response
|
||||
yield mock
|
14
py/lib/scwrypts/http/discord/__init__.py
Normal file
14
py/lib/scwrypts/http/discord/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
'''
|
||||
basic scwrypts.http client for discord
|
||||
|
||||
configured by setting various DISCORD__* options in the
|
||||
scwrypts environment
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'request',
|
||||
'send_message',
|
||||
]
|
||||
|
||||
from .client import request
|
||||
from .send_message import send_message
|
15
py/lib/scwrypts/http/discord/client.py
Normal file
15
py/lib/scwrypts/http/discord/client.py
Normal file
@ -0,0 +1,15 @@
|
||||
from scwrypts.env import getenv
|
||||
|
||||
from .. import get_request_client
|
||||
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
headers = {}
|
||||
|
||||
if (token := getenv("DISCORD__BOT_TOKEN", required = False)) is not None:
|
||||
headers['Authorization'] = f'Bot {token}'
|
||||
|
||||
return get_request_client(
|
||||
base_url = 'https://discord.com/api',
|
||||
headers = headers,
|
||||
)(method, endpoint, **kwargs)
|
24
py/lib/scwrypts/http/discord/conftest.py
Normal file
24
py/lib/scwrypts/http/discord/conftest.py
Normal file
@ -0,0 +1,24 @@
|
||||
from string import ascii_letters, digits
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from scwrypts.test.character_set import uri
|
||||
from ..conftest import generate, get_request_client_sample_data
|
||||
|
||||
@fixture(name='sample')
|
||||
def fixture_sample():
|
||||
return SimpleNamespace(
|
||||
**{
|
||||
**get_request_client_sample_data(),
|
||||
'base_url': 'https://discord.com/api',
|
||||
},
|
||||
bot_token = generate(str, {'character_set': uri}),
|
||||
username = generate(str, {'character_set': ascii_letters + digits}),
|
||||
avatar_url = generate(str, {'character_set': uri}),
|
||||
webhook = generate(str, {'character_set': uri}),
|
||||
channel_id = generate(str, {'character_set': uri}),
|
||||
content_header = generate(str),
|
||||
content_footer = generate(str),
|
||||
content = generate(str),
|
||||
)
|
48
py/lib/scwrypts/http/discord/send_message.py
Normal file
48
py/lib/scwrypts/http/discord/send_message.py
Normal file
@ -0,0 +1,48 @@
|
||||
from scwrypts.env import getenv
|
||||
from .client import request
|
||||
|
||||
def send_message(content, channel_id=None, webhook=None, username=None, avatar_url=None, **kwargs):
|
||||
if username is None:
|
||||
username = getenv('DISCORD__DEFAULT_USERNAME', 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'
|
||||
|
||||
elif (webhook := getenv('DISCORD__DEFAULT_WEBHOOK', required=False)) is not None:
|
||||
endpoint = f'webhooks/{webhook}'
|
||||
|
||||
elif (channel_id := getenv('DISCORD__DEFAULT_CHANNEL_ID', required=False)) is not None:
|
||||
endpoint = f'channels/{channel_id}/messages'
|
||||
|
||||
else:
|
||||
raise ValueError('must provide target channel_id or webhook')
|
||||
|
||||
if (header := getenv('DISCORD__CONTENT_HEADER', required=False)) is not None:
|
||||
content = f'{header}{content}'
|
||||
|
||||
if (footer := getenv('DISCORD__CONTENT_FOOTER', required=False)) is not None:
|
||||
content = f'{content}{footer}'
|
||||
|
||||
|
||||
return request(
|
||||
method = 'POST',
|
||||
endpoint = endpoint,
|
||||
json = {
|
||||
key: value
|
||||
for key, value in {
|
||||
'content': content,
|
||||
'username': username,
|
||||
'avatar_url': avatar_url,
|
||||
**kwargs,
|
||||
}.items()
|
||||
if value is not None
|
||||
},
|
||||
)
|
54
py/lib/scwrypts/http/discord/test_client.py
Normal file
54
py/lib/scwrypts/http/discord/test_client.py
Normal file
@ -0,0 +1,54 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .client import request
|
||||
|
||||
|
||||
def test_discord_request(sample, _mock_getenv, _response):
|
||||
assert _response == sample.response
|
||||
|
||||
def test_discord_request_client_setup(sample, mock_get_request_client, _mock_getenv, _response):
|
||||
mock_get_request_client.assert_called_once_with(
|
||||
base_url = sample.base_url,
|
||||
headers = { 'Authorization': f'Bot {sample.bot_token}' },
|
||||
)
|
||||
|
||||
def test_discord_request_client_setup_public(sample, mock_get_request_client, _mock_getenv_optional, _response):
|
||||
mock_get_request_client.assert_called_once_with(
|
||||
base_url = sample.base_url,
|
||||
headers = {},
|
||||
)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response')
|
||||
def fixture_response(sample):
|
||||
return request(
|
||||
method = sample.method,
|
||||
endpoint = sample.endpoint,
|
||||
**sample.payload,
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='mock_get_request_client', autouse=True)
|
||||
def fixture_mock_get_request_client(sample):
|
||||
with patch('scwrypts.http.discord.client.get_request_client') as mock:
|
||||
mock.return_value = lambda method, endpoint, **kwargs: sample.response
|
||||
yield mock
|
||||
|
||||
@fixture(name='_mock_getenv')
|
||||
def fixture_mock_getenv(sample):
|
||||
with patch('scwrypts.http.discord.client.getenv') as mock:
|
||||
mock.side_effect = lambda name, **kwargs: {
|
||||
'DISCORD__BOT_TOKEN': sample.bot_token,
|
||||
}[name]
|
||||
yield mock
|
||||
|
||||
@fixture(name='_mock_getenv_optional')
|
||||
def fixture_mock_getenv_optional():
|
||||
with patch('scwrypts.http.discord.client.getenv') as mock:
|
||||
mock.side_effect = lambda name, **kwargs: None
|
||||
yield mock
|
91
py/lib/scwrypts/http/discord/test_send_message.py
Normal file
91
py/lib/scwrypts/http/discord/test_send_message.py
Normal file
@ -0,0 +1,91 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture, raises
|
||||
|
||||
from .send_message import send_message
|
||||
|
||||
|
||||
def test_discord_send_message(sample, mock_request, _mock_getenv):
|
||||
expected = get_default_called_with(sample)
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_without_username(sample, mock_request, _mock_getenv):
|
||||
sample.username = None
|
||||
expected = get_default_called_with(sample)
|
||||
del expected['json']['username']
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_without_avatar_url(sample, mock_request, _mock_getenv):
|
||||
sample.avatar_url = None
|
||||
expected = get_default_called_with(sample)
|
||||
del expected['json']['avatar_url']
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_to_channel_id(sample, mock_request, _mock_getenv):
|
||||
sample.webhook = None
|
||||
expected = get_default_called_with(sample)
|
||||
expected['endpoint'] = f'channels/{sample.channel_id}/messages'
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_without_content_header(sample, mock_request, _mock_getenv):
|
||||
sample.content_header = None
|
||||
expected = get_default_called_with(sample)
|
||||
expected['json']['content'] = f'{sample.content}{sample.content_footer}'
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_without_content_footer(sample, mock_request, _mock_getenv):
|
||||
sample.content_footer = None
|
||||
expected = get_default_called_with(sample)
|
||||
expected['json']['content'] = f'{sample.content_header}{sample.content}'
|
||||
|
||||
assert send_message(sample.content) == sample.response
|
||||
mock_request.assert_called_once_with(**expected)
|
||||
|
||||
def test_discord_send_message_error(sample, mock_request, _mock_getenv):
|
||||
with raises(ValueError):
|
||||
sample.webhook = None
|
||||
sample.channel_id = None
|
||||
|
||||
send_message(sample.content)
|
||||
|
||||
#####################################################################
|
||||
|
||||
def get_default_called_with(sample):
|
||||
return {
|
||||
'method': 'POST',
|
||||
'endpoint': f'webhooks/{sample.webhook}',
|
||||
'json': {
|
||||
'content': f'{sample.content_header}{sample.content}{sample.content_footer}',
|
||||
'username': sample.username,
|
||||
'avatar_url': sample.avatar_url,
|
||||
},
|
||||
}
|
||||
|
||||
@fixture(name='mock_request', autouse=True)
|
||||
def fixture_mock_request(sample):
|
||||
with patch('scwrypts.http.discord.send_message.request') as mock:
|
||||
mock.return_value = sample.response
|
||||
yield mock
|
||||
|
||||
@fixture(name='_mock_getenv')
|
||||
def fixture_mock_getenv(sample):
|
||||
with patch('scwrypts.http.discord.send_message.getenv',) as mock:
|
||||
mock.side_effect = lambda name, **kwargs: {
|
||||
'DISCORD__DEFAULT_USERNAME': sample.username,
|
||||
'DISCORD__DEFAULT_AVATAR_URL': sample.avatar_url,
|
||||
'DISCORD__DEFAULT_WEBHOOK': sample.webhook,
|
||||
'DISCORD__DEFAULT_CHANNEL_ID': sample.channel_id,
|
||||
'DISCORD__CONTENT_HEADER': sample.content_header,
|
||||
'DISCORD__CONTENT_FOOTER': sample.content_footer,
|
||||
}[name]
|
||||
yield mock
|
14
py/lib/scwrypts/http/linear/__init__.py
Normal file
14
py/lib/scwrypts/http/linear/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
'''
|
||||
basic scwrypts.http client for linear
|
||||
|
||||
configured by setting the LINEAR__API_TOKEN option in the
|
||||
scwrypts environment
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'request',
|
||||
'graphql',
|
||||
]
|
||||
|
||||
from .client import request
|
||||
from .graphql import graphql
|
12
py/lib/scwrypts/http/linear/client.py
Normal file
12
py/lib/scwrypts/http/linear/client.py
Normal file
@ -0,0 +1,12 @@
|
||||
from scwrypts.env import getenv
|
||||
|
||||
from .. import get_request_client
|
||||
|
||||
|
||||
def request(method, endpoint, **kwargs):
|
||||
return get_request_client(
|
||||
base_url = 'https://api.linear.app',
|
||||
headers = {
|
||||
'Authorization': f'bearer {getenv("LINEAR__API_TOKEN")}',
|
||||
},
|
||||
)(method, endpoint, **kwargs)
|
18
py/lib/scwrypts/http/linear/conftest.py
Normal file
18
py/lib/scwrypts/http/linear/conftest.py
Normal file
@ -0,0 +1,18 @@
|
||||
from string import ascii_letters, digits
|
||||
from types import SimpleNamespace
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from scwrypts.test.character_set import uri
|
||||
from ..conftest import generate, get_request_client_sample_data
|
||||
|
||||
@fixture(name='sample')
|
||||
def fixture_sample():
|
||||
return SimpleNamespace(
|
||||
**{
|
||||
**get_request_client_sample_data(),
|
||||
'base_url': 'https://api.linear.app',
|
||||
},
|
||||
api_token = generate(str, {'character_set': uri}),
|
||||
query = generate(str),
|
||||
)
|
5
py/lib/scwrypts/http/linear/graphql.py
Normal file
5
py/lib/scwrypts/http/linear/graphql.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .client import request
|
||||
|
||||
|
||||
def graphql(query):
|
||||
return request('POST', 'graphql', json={'query': query})
|
42
py/lib/scwrypts/http/linear/test_client.py
Normal file
42
py/lib/scwrypts/http/linear/test_client.py
Normal file
@ -0,0 +1,42 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .client import request
|
||||
|
||||
|
||||
def test_discord_request(sample, _response):
|
||||
assert _response == sample.response
|
||||
|
||||
def test_discord_request_client_setup(sample, mock_get_request_client, _response):
|
||||
mock_get_request_client.assert_called_once_with(
|
||||
base_url = sample.base_url,
|
||||
headers = { 'Authorization': f'bearer {sample.api_token}' },
|
||||
)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response')
|
||||
def fixture_response(sample):
|
||||
return request(
|
||||
method = sample.method,
|
||||
endpoint = sample.endpoint,
|
||||
**sample.payload,
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='mock_get_request_client', autouse=True)
|
||||
def fixture_mock_get_request_client(sample):
|
||||
with patch('scwrypts.http.linear.client.get_request_client') as mock:
|
||||
mock.return_value = lambda method, endpoint, **kwargs: sample.response
|
||||
yield mock
|
||||
|
||||
@fixture(name='mock_getenv', autouse=True)
|
||||
def fixture_mock_getenv(sample):
|
||||
with patch('scwrypts.http.linear.client.getenv',) as mock:
|
||||
mock.side_effect = lambda name, **kwargs: {
|
||||
'LINEAR__API_TOKEN': sample.api_token,
|
||||
}[name]
|
||||
yield mock
|
35
py/lib/scwrypts/http/linear/test_graphql.py
Normal file
35
py/lib/scwrypts/http/linear/test_graphql.py
Normal file
@ -0,0 +1,35 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .graphql import graphql
|
||||
|
||||
|
||||
def test_directus_graphql(sample, _response, _mock_request):
|
||||
assert _response == sample.response
|
||||
|
||||
def test_directus_graphql_request_payload(sample, _response, _mock_request):
|
||||
_mock_request.assert_called_once_with(
|
||||
'POST',
|
||||
'graphql',
|
||||
json = {'query': sample.query},
|
||||
)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response')
|
||||
def fixture_response(sample, _mock_request):
|
||||
return graphql(sample.query)
|
||||
|
||||
|
||||
@fixture(name='_response_system')
|
||||
def fixture_response_system(sample, _mock_request):
|
||||
return graphql(sample.query)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_mock_request')
|
||||
def fixture_mock_request(sample):
|
||||
with patch('scwrypts.http.linear.graphql.request') as mock:
|
||||
mock.return_value = sample.response
|
||||
yield mock
|
55
py/lib/scwrypts/http/test_client.py
Normal file
55
py/lib/scwrypts/http/test_client.py
Normal file
@ -0,0 +1,55 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
from .client import get_request_client
|
||||
|
||||
|
||||
def test_request_client(sample, _response_basic):
|
||||
assert _response_basic == sample.response
|
||||
|
||||
def test_request_client_forwards_default_headers(sample, mock_request, _response_basic):
|
||||
mock_request.assert_called_once_with(
|
||||
method = sample.method,
|
||||
url = f'{sample.base_url}/{sample.endpoint}',
|
||||
headers = sample.headers,
|
||||
)
|
||||
|
||||
def test_get_request_client_payload(sample, _response_payload):
|
||||
assert _response_payload == sample.response
|
||||
|
||||
def test_request_client_forwards_payload_headers(sample, mock_request, _response_payload):
|
||||
assert mock_request.call_args.kwargs['headers'] == sample.headers | sample.payload_headers
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='mock_request', autouse=True)
|
||||
def fixture_mock_request(sample):
|
||||
with patch('scwrypts.http.client.request') as mock:
|
||||
mock.return_value = sample.response
|
||||
yield mock
|
||||
|
||||
@fixture(name='request_client', autouse=True)
|
||||
def fixture_request_client(sample):
|
||||
return get_request_client(sample.base_url, sample.headers)
|
||||
|
||||
#####################################################################
|
||||
|
||||
@fixture(name='_response_basic')
|
||||
def fixture_response_basic(sample, request_client):
|
||||
return request_client(
|
||||
method = sample.method,
|
||||
endpoint = sample.endpoint,
|
||||
)
|
||||
|
||||
@fixture(name='_response_payload')
|
||||
def fixture_response_payload(sample, request_client):
|
||||
return request_client(
|
||||
method = sample.method,
|
||||
endpoint = sample.endpoint,
|
||||
**{
|
||||
**sample.payload,
|
||||
'headers': sample.payload_headers,
|
||||
},
|
||||
)
|
1
py/lib/scwrypts/io/__init__.py
Normal file
1
py/lib/scwrypts/io/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .combined_io_stream import get_combined_stream, add_io_arguments
|
100
py/lib/scwrypts/io/combined_io_stream.py
Normal file
100
py/lib/scwrypts/io/combined_io_stream.py
Normal file
@ -0,0 +1,100 @@
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from sys import stdin, stdout, stderr
|
||||
|
||||
from scwrypts.env import getenv
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_combined_stream(input_file=None, output_file=None):
|
||||
'''
|
||||
context manager to open an "input_file" and "output_file"
|
||||
|
||||
But the "files" can be pipe-streams, stdin/stdout, or even
|
||||
actual files! Helpful when trying to write CLI scwrypts
|
||||
which would like to accept all kinds of input and output
|
||||
configurations.
|
||||
'''
|
||||
with get_stream(input_file, 'r') as input_stream, get_stream(output_file, 'w+') as output_stream:
|
||||
yield CombinedStream(input_stream, output_stream)
|
||||
|
||||
def add_io_arguments(parser, allow_input=True, allow_output=True):
|
||||
'''
|
||||
slap these puppies onto your argparse.ArgumentParser to
|
||||
allow easy use of the get_combined_stream at the command line
|
||||
'''
|
||||
if allow_input:
|
||||
parser.add_argument(
|
||||
'-i', '--input-file',
|
||||
dest = 'input_file',
|
||||
default = None,
|
||||
help = 'path to input file; omit for stdin',
|
||||
required = False,
|
||||
)
|
||||
|
||||
if allow_output:
|
||||
parser.add_argument(
|
||||
'-o', '--output-file',
|
||||
dest = 'output_file',
|
||||
default = None,
|
||||
help = 'path to output file; omit for stdout',
|
||||
required = False,
|
||||
)
|
||||
|
||||
|
||||
#####################################################################
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_stream(filename=None, mode='r', encoding='utf-8', verbose=False, **kwargs):
|
||||
allowed_modes = {'r', 'w', 'w+'}
|
||||
|
||||
if mode not in allowed_modes:
|
||||
raise ValueError(f'mode "{mode}" not supported modes (must be one of {allowed_modes})')
|
||||
|
||||
is_read = mode == 'r'
|
||||
|
||||
if filename is not None:
|
||||
|
||||
if verbose:
|
||||
print(f'opening file {filename} for {"read" if is_read else "write"}', file=stderr)
|
||||
|
||||
if filename[0] not in {'/', '~'}:
|
||||
filename = Path(f'{getenv("EXECUTION_DIR")}/{filename}').resolve()
|
||||
with open(filename, mode=mode, encoding=encoding, **kwargs) as stream:
|
||||
yield stream
|
||||
|
||||
else:
|
||||
if verbose:
|
||||
print('using stdin for read' if is_read else 'using stdout for write', file=stderr)
|
||||
|
||||
yield stdin if is_read else stdout
|
||||
|
||||
if not is_read:
|
||||
stdout.flush()
|
||||
|
||||
|
||||
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)
|
1
py/lib/scwrypts/redis/__init__.py
Normal file
1
py/lib/scwrypts/redis/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .client import get_client
|
@ -1,15 +1,19 @@
|
||||
from redis import StrictRedis
|
||||
|
||||
from py.scwrypts import getenv
|
||||
from scwrypts.env 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
|
23
py/lib/scwrypts/scwrypts/__init__.py
Normal file
23
py/lib/scwrypts/scwrypts/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
'''
|
||||
scwrypts meta-configuration
|
||||
|
||||
provides a helpful three ways to run "scwrypts"
|
||||
|
||||
'scwrypts' is an agnostic, top-level executor allowing any scwrypt to be called from python workflows
|
||||
|
||||
'execute' is the default context set-up for python-based scwrypts
|
||||
|
||||
'interactive' is a context set-up for interactive, python-based scwrypts
|
||||
after execution, you are dropped in a bpython shell with all the variables
|
||||
configured during main() execution
|
||||
'''
|
||||
|
||||
__all__ = [
|
||||
'scwrypts',
|
||||
'execute',
|
||||
'interactive',
|
||||
]
|
||||
|
||||
from .scwrypts import scwrypts
|
||||
from .execute import execute
|
||||
from .interactive import interactive
|
26
py/lib/scwrypts/scwrypts/exceptions.py
Normal file
26
py/lib/scwrypts/scwrypts/exceptions.py
Normal file
@ -0,0 +1,26 @@
|
||||
from argparse import ArgumentError
|
||||
|
||||
|
||||
class MissingVariableError(EnvironmentError):
|
||||
def init(self, name):
|
||||
super().__init__(f'Missing required environment variable "{name}"')
|
||||
|
||||
|
||||
class MissingFlagAndEnvironmentVariableError(EnvironmentError, ArgumentError):
|
||||
def __init__(self, flags, env_var):
|
||||
super().__init__(f'must provide at least one of : {{ flags: {flags} OR {env_var} }}')
|
||||
|
||||
|
||||
class MissingScwryptsExecutableError(EnvironmentError):
|
||||
def __init__(self):
|
||||
super().__init__('scwrypts must be installed and available on your PATH')
|
||||
|
||||
|
||||
class BadScwryptsLookupError(ValueError):
|
||||
def __init__(self):
|
||||
super().__init__('must provide name/group/type or scwrypt lookup patterns')
|
||||
|
||||
|
||||
class MissingScwryptsGroupOrTypeError(ValueError):
|
||||
def __init__(self, group, _type):
|
||||
super().__init__(f'missing required group or type (group={group} | type={_type}')
|
26
py/lib/scwrypts/scwrypts/execute.py
Normal file
26
py/lib/scwrypts/scwrypts/execute.py
Normal file
@ -0,0 +1,26 @@
|
||||
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
||||
|
||||
from scwrypts.io import get_combined_stream, add_io_arguments
|
||||
|
||||
|
||||
def execute(main, description=None, parse_args=None, allow_input=True, allow_output=True):
|
||||
'''
|
||||
API to initiate a python-based scwrypt
|
||||
'''
|
||||
if parse_args is None:
|
||||
parse_args = []
|
||||
|
||||
parser = ArgumentParser(
|
||||
description = description,
|
||||
formatter_class = ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
add_io_arguments(parser, allow_input, allow_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)
|
25
py/lib/scwrypts/scwrypts/interactive.py
Normal file
25
py/lib/scwrypts/scwrypts/interactive.py
Normal file
@ -0,0 +1,25 @@
|
||||
from bpython import embed
|
||||
|
||||
|
||||
def interactive(variable_descriptions):
|
||||
'''
|
||||
main() decorator to drop to interactive python environment upon completion
|
||||
'''
|
||||
def outer(function):
|
||||
|
||||
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
|
58
py/lib/scwrypts/scwrypts/scwrypts.py
Normal file
58
py/lib/scwrypts/scwrypts/scwrypts.py
Normal file
@ -0,0 +1,58 @@
|
||||
from os import getenv
|
||||
from shutil import which
|
||||
from subprocess import run
|
||||
|
||||
from .exceptions import MissingScwryptsExecutableError, BadScwryptsLookupError, MissingScwryptsGroupOrTypeError
|
||||
|
||||
|
||||
def scwrypts(patterns=None, args=None, executable_args=None, name=None, group=None, _type=None):
|
||||
'''
|
||||
top-level scwrypts invoker from python
|
||||
|
||||
patterns str / list pattern-based scwrypt lookup
|
||||
args str / list arguments forwarded to the invoked scwrypt
|
||||
executable_args str / list arguments for the 'scwrypts' executable
|
||||
(str above assumes space-delimited values)
|
||||
|
||||
name str exact scwrypt lookup name (requires group and _type)
|
||||
group str exact scwrypt lookup group
|
||||
_type str exact scwrypt lookup type
|
||||
|
||||
SCWRYPTS_EXECUTABLE configuration variable which defines the full path to scwrypts executable
|
||||
|
||||
see 'scwrypts --help' for more information
|
||||
'''
|
||||
if patterns is None and name is None:
|
||||
raise BadScwryptsLookupError()
|
||||
|
||||
if name is not None and (group is None or _type is None):
|
||||
raise MissingScwryptsGroupOrTypeError(group, _type)
|
||||
|
||||
executable = which(getenv('SCWRYPTS_EXECUTABLE', 'scwrypts'))
|
||||
|
||||
if executable is None:
|
||||
raise MissingScwryptsExecutableError()
|
||||
|
||||
lookup = _parse(patterns) if name is None else f'--name {name} --group {group} --type {_type}'
|
||||
|
||||
depth = getenv('SUBSCWRYPT', '')
|
||||
if depth != '':
|
||||
depth = int(depth) + 1
|
||||
|
||||
return run(
|
||||
f'SUBSCWRYPT={depth} {executable} {_parse(executable_args)} {lookup} -- {_parse(args)}',
|
||||
shell=True,
|
||||
executable='/bin/zsh',
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def _parse(string_or_list_args):
|
||||
if string_or_list_args is None:
|
||||
return ''
|
||||
|
||||
if isinstance(string_or_list_args, list):
|
||||
return ' '.join(string_or_list_args)
|
||||
|
||||
return str(string_or_list_args)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user