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

Big day! V4 is finally live. This INCLUDES some BREAKING CHANGES to ZSH
TYPE scwrypts! Please refer to the readme for upgrade details
                     (more specifically docs/upgrade/v3-to-v4.md)

Upgrade is SUPER EASY, so please take the time to do so.

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

- zsh type scwrypts have an upgraded runstring to improve context setup
  and simplicity to the scwrypt-writer

- scwrypts now publishes the package (scwrypts) to PyPi; this provides a
  simple way to invoke scwrypts from python-based environments as well
  as the entire scwrypts python library suite

  pip install scwrypts

- scwrypts now publishes the package (scwrypts) to npm; this provides a
  simple way to invoke scwrypts from nodesjs environments

  npm install scwrypts

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

- scwrypts runner prompts which use the zshbuiltin "read" now
  appropriately read input from tty, pipe, files, and user input

- virtualenv refresh now loads and prepares the scwrypts virtual
  environments correctly

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

- created the (-v, --log-level) scwrypts arguments as improvements of
  and replacements to the --verbose and --no-log flags
     - (-n) is now an alias for (--log-level 0)
     - (--no-log) is the same as (-n) for compatibility, but will be removed in 4.2

- zsh/lib/utils/io print functions now *interact with log-level* various
  log levels will now only display the appropriate console prints for
  the specified log level

- zsh/lib/utils/io:INFO has been renamed to DEBUG to align with
  log-level output; please use DEBUG for debug messages and REMINDER for
  important user messages

- created zsh/lib/utils/io:FZF_USER_INPUT as a *drop-in replacement* for
  the confusing FZF_HEAD and FZF_TAIL commands. Update by literally
  changing any instances of FZF_HEAD or FZF_TAIL with FZF_USER_INPUT
     - FZF_HEAD and FZF_TAIL will be removed in 4.2

- zsh/lib/utils/io:READ (and other zshbuiltin/read-based prompts) now
  accept a --force-user-input flag in case important checks should
  require an admin's approval. This flag will ensure that piped input
  and the `scwrypts -y` flag are ignored for the single prompt.

- zsh/lib/utils/color has been updated to use color names which match
  the ANSI color names

- zsh/hello-world has been reduced to a minimal example; this is to
  emphasize ease-of-use with v4

- zsh/sanity-check is a scwrypts/run testing helper and detailed
  starting reference (helpful since hello-world is now minimal)

- various refactor, updates, and improvements to the scwrypts runner

- migrated all zsh scwrypts and plugins to use v4 runner syntax
     - zsh
     - plugins/kubectl
     - plugins/ci

- refactored py/lib into py/lib/scwrypts (PyPi)
This commit is contained in:
2024-02-06 14:06:44 -07:00
parent a200c1eb22
commit a739d3b5a2
169 changed files with 15703 additions and 1451 deletions

View File

@ -0,0 +1 @@
from .client import get_request_client

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

View File

@ -0,0 +1,43 @@
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri
options = {
'str_length_minimum': 8,
'str_length_maximum': 128,
'uuid_output_type': str,
}
def get_request_client_sample_data():
return {
'base_url' : generate(str, options | {'character_set': uri}),
'endpoint' : generate(str, options | {'character_set': uri}),
'method' : generate(str, options),
'response' : generate('requests_Response', options | {'depth': 4}),
'payload' : generate(dict, {
**options,
'depth': 1,
'data_types': { str, 'uuid' },
}),
}
@fixture(name='sample')
def fixture_sample():
return SimpleNamespace(
**get_request_client_sample_data(),
headers = generate(dict, {
**options,
'depth': 1,
'data_types': { str, 'uuid' },
}),
payload_headers = generate(dict, {
**options,
'depth': 1,
'data_types': { str, 'uuid' },
}),
)

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

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

View 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

View File

@ -0,0 +1,16 @@
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri
from ..conftest import options, get_request_client_sample_data
@fixture(name='sample')
def fixture_sample():
return SimpleNamespace(
**get_request_client_sample_data(),
api_token = generate(str, options | {'character_set': uri}),
query = generate(str, options),
)

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

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

View 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

View 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

View 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

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

View File

@ -0,0 +1,25 @@
from string import ascii_letters, digits
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri
from ..conftest import options, 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, options | {'character_set': uri}),
username = generate(str, options | {'character_set': ascii_letters + digits}),
avatar_url = generate(str, options | {'character_set': uri}),
webhook = generate(str, options | {'character_set': uri}),
channel_id = generate(str, options | {'character_set': uri}),
content_header = generate(str, options),
content_footer = generate(str, options),
content = generate(str, options),
)

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

View File

@ -0,0 +1,54 @@
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, _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

View 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

View 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

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

View File

@ -0,0 +1,19 @@
from string import ascii_letters, digits
from types import SimpleNamespace
from pytest import fixture
from scwrypts.test import generate
from scwrypts.test.character_set import uri
from ..conftest import options, 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, options | {'character_set': uri}),
query = generate(str, options),
)

View File

@ -0,0 +1,5 @@
from .client import request
def graphql(query):
return request('POST', 'graphql', json={'query': query})

View 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

View 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

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