Fully convert Neural to use providers (#52)
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
.coverage
|
||||
.directory
|
||||
.tox
|
||||
*.pyc
|
||||
.tox-docker
|
||||
/env
|
||||
__pycache__
|
||||
tags
|
||||
# pyenv
|
||||
.python-version
|
||||
*.pyc
|
||||
*.egg-info
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.10.18
|
||||
+25
-12
@@ -28,26 +28,39 @@ RUN apk --update add $PACKAGES && \
|
||||
rm -rf /var/cache/apk/* /tmp/* /var/tmp/*
|
||||
|
||||
# Install tools for Python testing.
|
||||
ENV PATH /root/.pyenv/shims:/root/.pyenv/bin:$PATH
|
||||
# We need --ignore-installed to ignore the `packaging` package version.
|
||||
RUN pip install --ignore-installed tox==4.4.5
|
||||
RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash \
|
||||
&& eval "$(pyenv init -)" \
|
||||
&& eval "$(pyenv virtualenv-init -)" \
|
||||
&& pyenv install 3.7 \
|
||||
&& pyenv install 3.10 \
|
||||
&& ln -s /root/.pyenv/versions/3.7.*/bin/python3.7 /root/.pyenv/bin/python3.7 \
|
||||
&& ln -s /root/.pyenv/versions/3.10.*/bin/python3.10 /root/.pyenv/bin/python3.10
|
||||
ENV PATH=/root/.pyenv/shims:/root/.pyenv/bin:$PATH
|
||||
|
||||
# Switch to the /root dir copy the .python-version from the project.
|
||||
WORKDIR /root
|
||||
|
||||
# Install tools for Vim testing.
|
||||
# We have a layer here so we rebuild Vim and Neovim less frequently.
|
||||
# Installing the Vim versions is the slowest build step.
|
||||
RUN install_vim -tag v8.0.0027 -build \
|
||||
-tag v9.0.0297 -build \
|
||||
-tag neovim:v0.8.0 -build
|
||||
# Install vint with Python 3.10 to avoid `packaging` issues.
|
||||
RUN python3.10 -m pip install vim-vint==0.3.21
|
||||
RUN git clone https://github.com/junegunn/vader.vim vader && \
|
||||
cd vader && git checkout c6243dd81c98350df4dec608fa972df98fa2a3af
|
||||
|
||||
# Copy project files into the project for dependencies and such.
|
||||
COPY .python-version /root/
|
||||
|
||||
# Install the Python version we need with uv.
|
||||
# We have a layer here so we rebuild Python and install uv less frequently.
|
||||
# Installing Python with uv is slower than updating dependencies, but much
|
||||
# faster than installing the Vim and Neovim versions.
|
||||
RUN curl https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash \
|
||||
&& eval "$(pyenv init -)" \
|
||||
&& eval "$(pyenv virtualenv-init -)" \
|
||||
&& pyenv install \
|
||||
&& pip install uv
|
||||
|
||||
# Sync dependencies and install the Python dependencies we need.
|
||||
# vim-vint is included here for running the Vim lint steps.
|
||||
# We have a layer here that's very fast.
|
||||
COPY pyproject.toml uv.lock /root/
|
||||
RUN uv sync --locked --no-install-project
|
||||
|
||||
ARG GIT_VERSION
|
||||
LABEL Version=${GIT_VERSION}
|
||||
LABEL Name=denseanalysis/neural
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
[](https://www.vim.org/) [](https://neovim.io/) [](https://github.com/dense-analysis/neural/actions?query=event%3Apush+workflow%3ACI+branch%3Amain++) [](https://discord.gg/5zFD6pQxDk)
|
||||
|
||||
A ChatGPT Vim plugin, an OpenAI Neovim plugin, and so much more! Neural integrates various machine learning tools so you can let AI write code for you in Vim/Neovim, among other helpful things.
|
||||
A Neovim/Vim coding agent plugin. Neural integrates various machine learning
|
||||
tools so you can let AI write code for you in Neovim/Vim, among other helpful
|
||||
things. Use OpenAI's APIs made famous with ChatGPT, in Vim.
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
@@ -12,7 +14,7 @@ A ChatGPT Vim plugin, an OpenAI Neovim plugin, and so much more! Neural integrat
|
||||
* Easily ask AI to explain code or paragraphs `:NeuralExplain`
|
||||
* Compatible with Vim 8.0+ & Neovim 0.8+
|
||||
* Supported on Linux, Mac OSX, and Windows
|
||||
* Only dependency is Python 3.7+
|
||||
* Only dependency is Python 3.10+ (required for security and libraries)
|
||||
|
||||
Experience lightning-fast code generation and completion with asynchronous
|
||||
streaming.
|
||||
@@ -76,7 +78,7 @@ Plugin 'dense-analysis/neural'
|
||||
## 🚀 Usage
|
||||
|
||||
You will need to configure a third party machine learning tool for Neural to
|
||||
interact with. OpenAI is Neural's default data source, and one of the easiest
|
||||
interact with. OpenAI is Neural's default data provider, and one of the easiest
|
||||
to configure.
|
||||
|
||||
You will need to obtain an [OpenAI API key](https://beta.openai.com/signup/).
|
||||
@@ -86,20 +88,24 @@ script or in a Lua config.
|
||||
```vim
|
||||
" Configure Neural like so in Vimscript
|
||||
let g:neural = {
|
||||
\ 'source': {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ 'providers': [
|
||||
\ {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ },
|
||||
\ },
|
||||
\ },
|
||||
\ ],
|
||||
\}
|
||||
```
|
||||
|
||||
```lua
|
||||
-- Configure Neural like so in Lua
|
||||
require('neural').setup({
|
||||
source = {
|
||||
openai = {
|
||||
api_key = vim.env.OPENAI_API_KEY,
|
||||
providers = {
|
||||
{
|
||||
openai = {
|
||||
api_key = vim.env.OPENAI_API_KEY,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -129,6 +135,22 @@ the stop command by default when you enter that key combination. The default
|
||||
keybind can be disabled by setting `g:neural.set_default_keybinds` to any falsy
|
||||
value. You can set a keybind to stop Neural by mapping to `<Plug>(neural_stop)`.
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
To get started developing Neural, you will need to run the following commands,
|
||||
after first installing and correctly configuring
|
||||
[pyenv](https://github.com/pyenv/pyenv).
|
||||
|
||||
```sh
|
||||
pyenv install
|
||||
pip install uv
|
||||
uv sync
|
||||
```
|
||||
|
||||
You should then get all of the linters and static analysis tools, and you can
|
||||
run tests with `pytest` from virtualenv. We recommend using
|
||||
[ALE](https://github.com/dense-analysis/ale) to run linters for this project.
|
||||
|
||||
## 📜 Acknowledgements
|
||||
|
||||
Neural was created by [Anexon](https://github.com/Angelchev), and is maintained
|
||||
|
||||
+25
-28
@@ -1,8 +1,8 @@
|
||||
" Author: Anexon <anexon@protonmail.com>, w0rp <devw0rp@gmail.com>
|
||||
" Description: The main autoload file for the Neural Vim plugin
|
||||
|
||||
" The location of Neural source scripts
|
||||
let s:neural_script_dir = expand('<sfile>:p:h:h') . '/neural_providers'
|
||||
" The location of Neural provider scripts
|
||||
let s:neural_script_dir = expand('<sfile>:p:h:h') . '/src/neural/provider'
|
||||
" Keep track of the current job.
|
||||
let s:current_job = get(s:, 'current_job', 0)
|
||||
" Keep track of the line the last request happened on.
|
||||
@@ -117,8 +117,8 @@ function! s:HandleOutputEnd(buffer, job_data, exit_code) abort
|
||||
endfunction
|
||||
|
||||
" Get the path to the executable for a script language.
|
||||
function! s:GetScriptExecutable(source) abort
|
||||
if a:source.script_language is# 'python'
|
||||
function! s:GetScriptExecutable(provider) abort
|
||||
if a:provider.script_language is# 'python'
|
||||
let l:executable = ''
|
||||
|
||||
if has('win32')
|
||||
@@ -133,7 +133,7 @@ function! s:GetScriptExecutable(source) abort
|
||||
return l:executable
|
||||
endif
|
||||
|
||||
throw 'Unknown script language: ' . a:source.script_language
|
||||
throw 'Unknown script language: ' . a:provider.script_language
|
||||
endfunction
|
||||
|
||||
" Escape a string suitably for each platform.
|
||||
@@ -234,29 +234,26 @@ function! neural#PreProcess(buffer, input) abort
|
||||
endfor
|
||||
endfunction
|
||||
|
||||
function! s:LoadDataSource() abort
|
||||
let l:selected = g:neural.selected
|
||||
function! s:LoadProvider() abort
|
||||
" TODO: Change message if nothing is selected.
|
||||
let l:config = get(g:neural.providers, 0, {})
|
||||
let l:type = get(l:config, 'type', '')
|
||||
|
||||
try
|
||||
let l:source = function('neural#source#' . selected . '#Get')()
|
||||
let l:provider = function('neural#provider#' . l:type . '#Get')()
|
||||
catch /E117/
|
||||
call neural#OutputErrorMessage('Invalid source: ' . l:selected)
|
||||
call neural#OutputErrorMessage('Invalid provider: ' . l:type)
|
||||
|
||||
return
|
||||
endtry
|
||||
|
||||
return l:source
|
||||
let l:provider.config = config
|
||||
|
||||
return l:provider
|
||||
endfunction
|
||||
|
||||
function! s:GetSourceInput(buffer, source, prompt) abort
|
||||
let l:config = get(g:neural.source, a:source.name, {})
|
||||
|
||||
" If the config is not a Dictionary, throw it away.
|
||||
if type(l:config) isnot v:t_dict
|
||||
let l:config = {}
|
||||
endif
|
||||
|
||||
let l:input = {'config': l:config, 'prompt': a:prompt}
|
||||
function! s:GetProviderInput(buffer, provider, prompt) abort
|
||||
let l:input = {'config': a:provider.config, 'prompt': a:prompt}
|
||||
|
||||
" Pre-process input, such as modifying a prompt.
|
||||
call neural#PreProcess(a:buffer, l:input)
|
||||
@@ -265,13 +262,13 @@ function! s:GetSourceInput(buffer, source, prompt) abort
|
||||
endfunction
|
||||
|
||||
function! neural#GetCommand(buffer) abort
|
||||
let l:source = s:LoadDataSource()
|
||||
let l:script_exe = s:GetScriptExecutable(l:source)
|
||||
let l:provider = s:LoadProvider()
|
||||
let l:script_exe = s:GetScriptExecutable(l:provider)
|
||||
let l:command = neural#Escape(l:script_exe)
|
||||
\ . ' ' . neural#Escape(l:source.script)
|
||||
\ . ' ' . neural#Escape(l:provider.script)
|
||||
let l:command = neural#job#PrepareCommand(a:buffer, l:command)
|
||||
|
||||
return [l:source, l:command]
|
||||
return [l:provider, l:command]
|
||||
endfunction
|
||||
|
||||
function! neural#Prompt(prompt) abort
|
||||
@@ -300,8 +297,8 @@ function! neural#ViewPrompt(...) abort
|
||||
" Take the first argument or nothing.
|
||||
let l:prompt = get(a:000, 0, '')
|
||||
let l:buffer = bufnr('')
|
||||
let l:source = s:LoadDataSource()
|
||||
let l:input = s:GetSourceInput(l:buffer, l:source, l:prompt)
|
||||
let l:provider = s:LoadProvider()
|
||||
let l:input = s:GetProviderInput(l:buffer, l:provider, l:prompt)
|
||||
|
||||
" no-custom-checks
|
||||
echohl Question
|
||||
@@ -330,7 +327,7 @@ function! neural#Run(prompt, options) abort
|
||||
let l:moving_line -= 1
|
||||
endif
|
||||
|
||||
let [l:source, l:command] = neural#GetCommand(l:buffer)
|
||||
let [l:provider, l:command] = neural#GetCommand(l:buffer)
|
||||
let l:job_data = {
|
||||
\ 'moving_line': l:moving_line,
|
||||
\ 'error_lines': [],
|
||||
@@ -344,10 +341,10 @@ function! neural#Run(prompt, options) abort
|
||||
\})
|
||||
|
||||
if l:job_id > 0
|
||||
let l:input = s:GetSourceInput(l:buffer, l:source, a:prompt)
|
||||
let l:input = s:GetProviderInput(l:buffer, l:provider, a:prompt)
|
||||
call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n")
|
||||
else
|
||||
call neural#OutputErrorMessage('Failed to run ' . l:source.name)
|
||||
call neural#OutputErrorMessage('Failed to run ' . l:provider.name)
|
||||
|
||||
return
|
||||
endif
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
" Author: Anexon <anexon@protonmail.com>
|
||||
" Description: A Neural Scratch Buffer acts as a playground for interacting with
|
||||
" Neural providers directly, sending all content of the buffer to the source.
|
||||
" Neural providers directly, sending all content of the buffer to the provider.
|
||||
|
||||
scriptencoding utf-8
|
||||
|
||||
@@ -9,7 +9,7 @@ call neural#config#Load()
|
||||
function! s:GetOptions(options_dict_string) abort
|
||||
call neural#config#Load()
|
||||
|
||||
" TODO: Set buffer name based on source.
|
||||
" TODO: Set buffer name based on provider.
|
||||
let l:options = {
|
||||
\ 'name': 'Neural Buffer',
|
||||
\ 'create_mode': g:neural.buffer.create_mode,
|
||||
@@ -33,6 +33,8 @@ function! s:GetOptions(options_dict_string) abort
|
||||
endif
|
||||
endif
|
||||
|
||||
Dump l:options
|
||||
|
||||
return l:options
|
||||
endfunction
|
||||
|
||||
@@ -42,6 +44,10 @@ function! neural#buffer#CreateBuffer(options) abort
|
||||
" TODO: Add auto incrementing buffer names instead of switching.
|
||||
if bufexists(l:buffer_options.name)
|
||||
execute 'buffer' bufnr(l:buffer_options.name)
|
||||
setlocal filetype=neuralbuf
|
||||
setlocal buftype=nofile
|
||||
setlocal bufhidden=hide
|
||||
setlocal noswapfile
|
||||
else
|
||||
if l:buffer_options.create_mode is# 'vertical'
|
||||
vertical new
|
||||
|
||||
+84
-28
@@ -5,8 +5,24 @@ scriptencoding utf-8
|
||||
" Track modifications to g:neural, in case we set it again.
|
||||
let s:last_dictionary = get(s:, 'last_dictionary', {})
|
||||
|
||||
" Defaults for different providers.
|
||||
let s:provider_defaults = {
|
||||
\ 'openai': {
|
||||
\ 'type': 'openai',
|
||||
\ 'url': 'https://api.openai.com',
|
||||
\ 'api_key': '',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 1024,
|
||||
\ 'model': 'gpt-3.5-turbo-instruct',
|
||||
\ 'use_chat_api': v:false,
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ },
|
||||
\}
|
||||
|
||||
" Defaults for general settings.
|
||||
let s:defaults = {
|
||||
\ 'selected': 'openai',
|
||||
\ 'pre_process': {
|
||||
\ 'enabled': v:true,
|
||||
\ },
|
||||
@@ -22,26 +38,7 @@ let s:defaults = {
|
||||
\ 'create_mode': 'vertical',
|
||||
\ 'wrap': v:true,
|
||||
\ },
|
||||
\ 'source': {
|
||||
\ 'openai': {
|
||||
\ 'api_key': '',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 1024,
|
||||
\ 'model': 'gpt-3.5-turbo-instruct',
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ },
|
||||
\ 'chatgpt': {
|
||||
\ 'api_key': '',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 2048,
|
||||
\ 'model': 'gpt-3.5-turbo',
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ },
|
||||
\ },
|
||||
\ 'providers': [],
|
||||
\}
|
||||
|
||||
function! neural#config#DeepMerge(into, from) abort
|
||||
@@ -56,28 +53,87 @@ function! neural#config#DeepMerge(into, from) abort
|
||||
return a:into
|
||||
endfunction
|
||||
|
||||
function! s:ApplySpecialDefaults() abort
|
||||
if empty(g:neural.source.chatgpt.api_key)
|
||||
let g:neural.source.chatgpt.api_key = g:neural.source.openai.api_key
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" Set the shared configuration for Neural.
|
||||
function! neural#config#Set(settings) abort
|
||||
let g:neural = a:settings
|
||||
call neural#config#Load()
|
||||
endfunction
|
||||
|
||||
function! neural#config#ConvertLegacySettings(dictionary) abort
|
||||
" Use the old selection to know which source to copy to providers.
|
||||
let l:selected = get(a:dictionary, 'selected', 'openai')
|
||||
|
||||
" Replace 'source' with newer 'providers'
|
||||
if has_key(a:dictionary, 'source') && !has_key(a:dictionary, 'providers')
|
||||
let l:source = remove(a:dictionary, 'source')
|
||||
let a:dictionary.providers = []
|
||||
|
||||
if type(l:source) is v:t_dict
|
||||
" Default the configuration to the openai config for chatgpt.
|
||||
let l:settings = get(l:source, l:selected, get(l:source, 'openai'))
|
||||
" Keep old behavior to default the chatgpt key to the openai key.
|
||||
let l:default_api_key = get(get(l:source, 'openai', {}), 'api_key', '')
|
||||
|
||||
if type(l:settings) is v:t_dict
|
||||
let l:settings = copy(l:settings)
|
||||
let l:settings.type = l:selected
|
||||
|
||||
if l:selected is# 'chatgpt'
|
||||
let l:settings.use_chat_api = v:true
|
||||
endif
|
||||
|
||||
if empty(get(l:settings, 'api_key'))
|
||||
let l:settings.api_key = l:default_api_key
|
||||
endif
|
||||
|
||||
call add(a:dictionary.providers, l:settings)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
" Remove the 'selected' key if set.
|
||||
if has_key(a:dictionary, 'selected')
|
||||
call remove(a:dictionary, 'selected')
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" Merge defaults for different provider types in.
|
||||
function! neural#config#MergeProviderDefaults(providers) abort
|
||||
let l:merged_providers = []
|
||||
|
||||
if type(a:providers) is v:t_list && !empty(a:providers)
|
||||
for l:source in a:providers
|
||||
let l:type = get(l:source, 'type', v:null)
|
||||
|
||||
call add(l:merged_providers, neural#config#DeepMerge(
|
||||
\ deepcopy(get(s:provider_defaults, l:type, {})),
|
||||
\ l:source,
|
||||
\))
|
||||
endfor
|
||||
else
|
||||
" Default providers to just openai with no API key.
|
||||
let l:merged_providers = [deepcopy(s:provider_defaults.openai)]
|
||||
endif
|
||||
|
||||
return l:merged_providers
|
||||
endfunction
|
||||
|
||||
function! neural#config#Load() abort
|
||||
let l:dictionary = get(g:, 'neural', {})
|
||||
|
||||
" Merge the Dictionary with defaults again if g:neural changed.
|
||||
if l:dictionary isnot# s:last_dictionary
|
||||
" Create a shallow copy to modify
|
||||
let l:dictionary = copy(l:dictionary)
|
||||
call neural#config#ConvertLegacySettings(l:dictionary)
|
||||
let l:dictionary.providers = neural#config#MergeProviderDefaults(
|
||||
\ get(l:dictionary, 'providers', v:null)
|
||||
\)
|
||||
|
||||
let s:last_dictionary = neural#config#DeepMerge(
|
||||
\ deepcopy(s:defaults),
|
||||
\ l:dictionary,
|
||||
\)
|
||||
let g:neural = s:last_dictionary
|
||||
call s:ApplySpecialDefaults()
|
||||
endif
|
||||
endfunction
|
||||
|
||||
@@ -52,7 +52,7 @@ function! neural#explain#SelectedLines() abort
|
||||
let l:range = neural#visual#GetRange()
|
||||
let l:buffer = bufnr('')
|
||||
|
||||
let [l:source, l:command] = neural#GetCommand(l:buffer)
|
||||
let [l:provider, l:command] = neural#GetCommand(l:buffer)
|
||||
|
||||
let l:job_data = {
|
||||
\ 'output_lines': [],
|
||||
@@ -68,20 +68,13 @@ function! neural#explain#SelectedLines() abort
|
||||
if l:job_id > 0
|
||||
let l:lines = neural#redact#PasswordsAndSecrets(l:range.selection)
|
||||
|
||||
let l:config = get(g:neural.source, l:source.name, {})
|
||||
|
||||
" If the config is not a Dictionary, throw it away.
|
||||
if type(l:config) isnot v:t_dict
|
||||
let l:config = {}
|
||||
endif
|
||||
|
||||
let l:input = {
|
||||
\ 'config': l:config,
|
||||
\ 'config': l:provider.config,
|
||||
\ 'prompt': "Explain these lines:\n\n" . join(l:lines, "\n"),
|
||||
\}
|
||||
call neural#job#SendRaw(l:job_id, json_encode(l:input) . "\n")
|
||||
else
|
||||
call neural#OutputErrorMessage('Failed to run ' . l:source.name)
|
||||
call neural#OutputErrorMessage('Failed to run ' . l:provider.name)
|
||||
|
||||
return
|
||||
endif
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
" Author: w0rp <devw0rp@gmail.com>
|
||||
" Description: A script describing how to use OpenAI with Neural
|
||||
" Description: A script describing how to use OpenAI compatible APIs with Neural
|
||||
|
||||
function! neural#source#openai#Get() abort
|
||||
function! neural#provider#openai#Get() abort
|
||||
return {
|
||||
\ 'name': 'openai',
|
||||
\ 'script_language': 'python',
|
||||
@@ -1,10 +0,0 @@
|
||||
" Author: w0rp <devw0rp@gmail.com>
|
||||
" Description: A script describing how to use ChatGPT with Neural
|
||||
|
||||
function! neural#source#chatgpt#Get() abort
|
||||
return {
|
||||
\ 'name': 'chatgpt',
|
||||
\ 'script_language': 'python',
|
||||
\ 'script': neural#GetScriptDir() . '/chatgpt.py',
|
||||
\}
|
||||
endfunction
|
||||
@@ -124,21 +124,18 @@ Apply the following rules when writing Bash scripts.
|
||||
===============================================================================
|
||||
4. Development Environment *neural-development-environment*
|
||||
|
||||
You can run `tox` without Docker if you install the required Python versions.
|
||||
Neural tests against Python 3.7 and Python 3.10.
|
||||
|
||||
Run the following: >
|
||||
python -m pip install --user tox==4.4.5
|
||||
python -m tox
|
||||
pyenv install
|
||||
pip install uv
|
||||
uv sync
|
||||
<
|
||||
To get the same configuration in your editor, run `./install-python-env.sh`.
|
||||
ALE should pick up on how to run `flake8` and `pyright` automatically, but you
|
||||
may wish to configure ALE to run only those linters. You can configure ALE
|
||||
to fix the Python files with `isort`. Apply the following settings: >
|
||||
This should install all Python dependencies for development including
|
||||
`pyright`, `ruff`, etc. You may wish to configure ALE to run only the linters
|
||||
and fixers for the project. You can configure ALE like so: >
|
||||
|
||||
if expand('%:p') =~# 'neural'
|
||||
let b:ale_linters = ['flake8', 'pyright']
|
||||
let b:ale_fixers = ['isort']
|
||||
let b:ale_linters = ['ruff', 'pyright']
|
||||
let b:ale_fixers = ['ruff']
|
||||
endif
|
||||
<
|
||||
|
||||
|
||||
+63
-112
@@ -11,8 +11,7 @@ CONTENTS *neural-contents*
|
||||
4.1 UI ........................................ |neural-ui|
|
||||
4.2 Neural Buffer ............................. |neural-buffer|
|
||||
4.3 OpenAI .................................... |neural-openai|
|
||||
4.4 ChatGPT ................................... |neural-chatgpt|
|
||||
4.5 Highlights ................................ |neural-highlights|
|
||||
4.4 Highlights ................................ |neural-highlights|
|
||||
5. API ........................................ |neural-api|
|
||||
6. Environment Variables ...................... |neural-env|
|
||||
6.1 Linux + KDE ............................. |neural-env-kde|
|
||||
@@ -32,10 +31,11 @@ generate text, code, and much more.
|
||||
|
||||
Neural supports the following tools.
|
||||
|
||||
1. OpenAI - https://beta.openai.com/signup/
|
||||
1. OpenAI - https://platform.openai.com/signup
|
||||
2. Any model that uses the OpenAI API. See |neural-provider-openai.url|
|
||||
|
||||
To select the tool that Neural will use, set |g:neural.selected| to the
|
||||
appropriate value. OpenAI is the default data source.
|
||||
To select the tool that Neural will use, set |g:neural.providers| to the
|
||||
appropriate value. OpenAI is the default data provider.
|
||||
|
||||
===============================================================================
|
||||
3. Commands/Keybinds *neural-commands*
|
||||
@@ -71,7 +71,7 @@ appropriate value. OpenAI is the default data source.
|
||||
`:NeuralCompletion` *NeuralCompletion*
|
||||
|
||||
A command for a |NeuralBuffer| (`neuralbuf` filetype) that sends all buffer
|
||||
contents to the current neural source for completion and appends the
|
||||
contents to the current neural provider for completion and appends the
|
||||
response to the buffer.
|
||||
|
||||
A `neuralbuf` plug mapping `<Plug>(neural_completion)` is defined for this
|
||||
@@ -81,8 +81,8 @@ appropriate value. OpenAI is the default data source.
|
||||
`:NeuralExplain` *NeuralExplain*
|
||||
|
||||
A |visual-mode| command for explaining the highlighted lines. The visual
|
||||
selection will be sent to the currently selected source model and the
|
||||
response will be displayed in a preview window.
|
||||
selection will be sent to the currently selected provider and the response
|
||||
will be displayed in a preview window.
|
||||
|
||||
Neural will make basic attempts to redact lines that appear to contain
|
||||
passwords or secrets.
|
||||
@@ -124,11 +124,13 @@ configured either in Vim or in Lua.
|
||||
In Vim just set `g:neural`: >
|
||||
|
||||
let g:neural = {
|
||||
\ 'source': {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ 'providers': [
|
||||
\ {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ },
|
||||
\ },
|
||||
\ },
|
||||
\ ],
|
||||
\}
|
||||
<
|
||||
In a Neovim `init.lua` call `require('neural').setup`: >
|
||||
@@ -137,10 +139,12 @@ In a Neovim `init.lua` call `require('neural').setup`: >
|
||||
ui = {
|
||||
animated_sign_enabled = false,
|
||||
},
|
||||
source = {
|
||||
openai = {
|
||||
api_key = vim.env.OPENAI_API_KEY,
|
||||
},
|
||||
providers = {
|
||||
{
|
||||
openai = {
|
||||
api_key = vim.env.OPENAI_API_KEY,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
<
|
||||
@@ -151,16 +155,6 @@ options listed below.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
g:neural.selected *g:neural.selected*
|
||||
*vim.g.neural.selected*
|
||||
Type: |String|
|
||||
Default: `'openai'`
|
||||
|
||||
Selects a source for Neural to use. Possible values are:
|
||||
|
||||
1. `'openai'` - OpenAI
|
||||
2. `'chatgpt'` - ChatGPT
|
||||
|
||||
|
||||
g:neural.set_default_keybinds *g:neural.set_default_keybinds*
|
||||
*vim.g.neural.set_default_keybinds*
|
||||
@@ -183,6 +177,16 @@ g:neural.pre_process.enabled *g:neural.pre_process.enabled*
|
||||
Neural edits the text you send automatically by default to improve the
|
||||
quality of prompts to produce better results for each filetype.
|
||||
|
||||
g:neural.providers *g:neural.providres*
|
||||
*vim.g.neural.providers*
|
||||
Type: |List|
|
||||
Default: `[]`
|
||||
|
||||
The List of providers to configure which providers Neural will use.
|
||||
|
||||
NOTE: At the moment Neural will only ever use the first provider, and
|
||||
ignore the rest. If unspecified, OpenAI will be used by default.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
4.1 UI *neural-ui*
|
||||
@@ -266,20 +270,20 @@ g:neural.buffer.wrap *g:neural.buffer.wrap*
|
||||
-------------------------------------------------------------------------------
|
||||
4.3 OpenAI *neural-openai*
|
||||
|
||||
Options for configuring OpenAI are listed below.
|
||||
Options for configuring OpenAI are listed below. This settings should be set
|
||||
as an Dictionary/table in the `providers` List with `type` set to `'openai'`.
|
||||
|
||||
|
||||
g:neural.source.openai.api_key *g:neural.source.openai.api_key*
|
||||
*vim.g.neural.source.openai.api_key*
|
||||
api_key *neural-provider-openai.api_key*
|
||||
|
||||
Type: |String|
|
||||
Default: `''`
|
||||
|
||||
The OpenAI API key. See: https://beta.openai.com/signup/
|
||||
|
||||
|
||||
g:neural.source.openai.frequency_penalty
|
||||
*g:neural.source.openai.frequency_penalty*
|
||||
*vim.g.neural.source.openai.frequency_penalty*
|
||||
frequency_penalty
|
||||
*neural-provider-openai.frequency_penalty*
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.1`
|
||||
|
||||
@@ -290,8 +294,8 @@ g:neural.source.openai.frequency_penalty
|
||||
See: https://platform.openai.com/docs/api-reference/parameter-details
|
||||
|
||||
|
||||
g:neural.source.openai.max_tokens *g:neural.source.openai.max_tokens*
|
||||
*vim.g.neural.source.openai.max_tokens*
|
||||
max_tokens *neural-provider-openai.max_tokens*
|
||||
|
||||
Type: |Number|
|
||||
Default: `1024`
|
||||
|
||||
@@ -301,18 +305,20 @@ g:neural.source.openai.max_tokens *g:neural.source.openai.max_tokens*
|
||||
This translates to roughly `¾` of a word (e.g. `100 tokens ~= 75 words`).
|
||||
|
||||
|
||||
g:neural.source.openai.model *g:neural.source.openai.model*
|
||||
*vim.g.neural.source.openai.model*
|
||||
model *neural-provider-openai.model*
|
||||
|
||||
Type: |String|
|
||||
Default: `'gpt-3.5-turbo-instruct'`
|
||||
|
||||
The model to use for OpenAI. Please consult OpenAI's documentation for more
|
||||
information on models: https://platform.openai.com/docs/models/overview
|
||||
|
||||
See |neural-provider-openai.use_chat_api| if changing models, as you may
|
||||
need to use the chat API only for newer models.
|
||||
|
||||
g:neural.source.openai.presence_penalty
|
||||
*g:neural.source.openai.presence_penalty*
|
||||
*vim.g.neural.source.openai.presence_penalty*
|
||||
|
||||
presence_penalty
|
||||
*neural-provider-openai.presence_penalty*
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.1`
|
||||
|
||||
@@ -324,8 +330,8 @@ g:neural.source.openai.presence_penalty
|
||||
See: https://platform.openai.com/docs/api-reference/parameter-details
|
||||
|
||||
|
||||
g:neural.source.openai.temperature *g:neural.source.openai.temperature*
|
||||
*vim.g.neural.source.openai.temperature*
|
||||
temperature *neural-provider-openai.temperature*
|
||||
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.2`
|
||||
|
||||
@@ -335,8 +341,8 @@ g:neural.source.openai.temperature *g:neural.source.openai.temperature*
|
||||
like `0.2` will make it more focused and deterministic.
|
||||
|
||||
|
||||
g:neural.source.openai.top_p *g:neural.source.openai.top_p*
|
||||
*vim.g.neural.source.openai.top_p*
|
||||
top_p *neural-provider-openai.top_p*
|
||||
|
||||
Type: |Number| or |Float|
|
||||
Default: `1`
|
||||
|
||||
@@ -351,75 +357,18 @@ g:neural.source.openai.top_p *g:neural.source.openai.top_p*
|
||||
OpenAI recommends altering this or temperature but not both.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
4.4 ChatGPT *neural-chatgpt*
|
||||
use_chat_api *neural-provider-openai.use_chat_api*
|
||||
Type: |Boolean|
|
||||
Default: `true`
|
||||
|
||||
Options for configuring ChatGPT are listed below.
|
||||
|
||||
Many options are simliar to OpenAI completion options above.
|
||||
|
||||
|
||||
g:neural.source.chatgpt.api_key *g:neural.source.chatgpt.api_key*
|
||||
*vim.g.neural.source.chatgpt.api_key*
|
||||
Type: |String|
|
||||
Default: `''`
|
||||
|
||||
The OpenAI API key. See: https://beta.openai.com/signup/
|
||||
|
||||
|
||||
g:neural.source.chatgpt.frequency_penalty
|
||||
*g:neural.source.chatgpt.frequency_penalty*
|
||||
*vim.g.neural.source.chatgpt.frequency_penalty*
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.1`
|
||||
|
||||
See Also: |g:neural.source.openai.frequency_penalty|
|
||||
|
||||
|
||||
g:neural.source.chatgpt.max_tokens *g:neural.source.chatgpt.max_tokens*
|
||||
*vim.g.neural.source.chatgpt.max_tokens*
|
||||
Type: |Number|
|
||||
Default: `2048`
|
||||
|
||||
See Also: |g:neural.source.openai.max_tokens|
|
||||
|
||||
|
||||
g:neural.source.chatgpt.model *g:neural.source.chatgpt.model*
|
||||
*vim.g.neural.source.chatgpt.model*
|
||||
Type: |String|
|
||||
Default: `'gpt-3.5-turbo'`
|
||||
|
||||
The model to use for ChatGPT. Please consult OpenAI's documentation for more
|
||||
information on models: https://platform.openai.com/docs/models/overview
|
||||
|
||||
|
||||
g:neural.source.chatgpt.presence_penalty
|
||||
*g:neural.source.chatgpt.presence_penalty*
|
||||
*vim.g.neural.source.chatgpt.presence_penalty*
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.1`
|
||||
|
||||
See Also: |g:neural.source.openai.presence_penalty|
|
||||
|
||||
|
||||
g:neural.source.chatgpt.temperature *g:neural.source.chatgpt.temperature*
|
||||
*vim.g.neural.source.chatgpt.temperature*
|
||||
Type: |Number| or |Float|
|
||||
Default: `0.2`
|
||||
|
||||
See Also: |g:neural.source.openai.temperature|
|
||||
|
||||
|
||||
g:neural.source.chatgpt.top_p *g:neural.source.chatgpt.top_p*
|
||||
*vim.g.neural.source.chatgpt.top_p*
|
||||
Type: |Number| or |Float|
|
||||
Default: `1`
|
||||
|
||||
See Also: |g:neural.source.openai.top_p|
|
||||
For configuring if Neural should use `/v1/chat/completions` if `true`, or
|
||||
the `/v1/completions` API if `false`. Older models such as
|
||||
`'gpt-3.5-turbo-instruct'` will only run with the completions API, and
|
||||
newer models may only run with the chat API.
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
4.5 Highlights *neural-highlights*
|
||||
4.4 Highlights *neural-highlights*
|
||||
|
||||
The following highlights can be configured to change |neural|'s colors.
|
||||
|
||||
@@ -483,11 +432,13 @@ out and in again, you will be able to access your environment variables in any
|
||||
Vim or GUI Vim startup script you like uniformly, like so: >
|
||||
|
||||
let g:neural = {
|
||||
\ 'source': {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ 'providers': [
|
||||
\ {
|
||||
\ 'openai': {
|
||||
\ 'api_key': $OPENAI_API_KEY,
|
||||
\ },
|
||||
\ },
|
||||
\ },
|
||||
\ ],
|
||||
\}
|
||||
<
|
||||
All applications on your system will also be able to read the same environment
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script is not necessary for running the Neural plugin.
|
||||
#
|
||||
# This is merely for making it easy for Neural developers to develop the Python
|
||||
# parts.
|
||||
#
|
||||
# ALE is recommended for linting and fixing Python files.
|
||||
|
||||
set -eu
|
||||
|
||||
if ! [ -d env ]; then
|
||||
python3.10 -m venv env
|
||||
fi
|
||||
|
||||
env/bin/pip install -r test-requirements.txt
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
A Neural datasource for loading generated text via OpenAI.
|
||||
"""
|
||||
import json
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any, Dict
|
||||
|
||||
API_ENDPOINT = 'https://api.openai.com/v1/chat/completions'
|
||||
|
||||
OPENAI_DATA_HEADER = 'data: '
|
||||
OPENAI_DONE = '[DONE]'
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
The sanitised configuration.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
model: str,
|
||||
temperature: float,
|
||||
top_p: float,
|
||||
max_tokens: int,
|
||||
presence_penalty: float,
|
||||
frequency_penalty: float,
|
||||
):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.temperature = temperature
|
||||
self.top_p = top_p
|
||||
self.max_tokens = max_tokens
|
||||
self.presence_penalty = presence_penalty
|
||||
self.frequency_penalty = frequency_penalty
|
||||
|
||||
|
||||
def get_openai_completion(config: Config, prompt: str) -> None:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {config.api_key}"
|
||||
}
|
||||
data = {
|
||||
"model": config.model,
|
||||
"prompt": prompt,
|
||||
"temperature": config.temperature,
|
||||
"max_tokens": config.max_tokens,
|
||||
"top_p": 1,
|
||||
"presence_penalty": config.presence_penalty,
|
||||
"frequency_penalty": config.frequency_penalty,
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
API_ENDPOINT,
|
||||
data=json.dumps(data).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
unverifiable=True,
|
||||
)
|
||||
|
||||
# Disable SSL certificate verification on macOS.
|
||||
# This is bad for security, and we need to deal with SSL errors better.
|
||||
#
|
||||
# This is the error:
|
||||
# urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)> # noqa
|
||||
context = (
|
||||
ssl._create_unverified_context() # type: ignore
|
||||
if platform.system() == "Darwin" else
|
||||
None
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req, context=context) as response:
|
||||
while True:
|
||||
line_bytes = response.readline()
|
||||
|
||||
if not line_bytes:
|
||||
break
|
||||
|
||||
line = line_bytes.decode("utf-8", errors="replace")
|
||||
|
||||
if line.startswith(OPENAI_DATA_HEADER):
|
||||
line_data = line[len(OPENAI_DATA_HEADER):-1]
|
||||
|
||||
if line_data == OPENAI_DONE:
|
||||
pass
|
||||
else:
|
||||
openai_obj = json.loads(line_data)
|
||||
|
||||
print(openai_obj["choices"][0]["text"], end="", flush=True)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def load_config(raw_config: Dict[str, Any]) -> Config:
|
||||
# TODO: Add range validation for request parameters.
|
||||
if not isinstance(raw_config, dict): # type: ignore
|
||||
raise ValueError("openai config is not a dictionary")
|
||||
|
||||
api_key = raw_config.get('api_key')
|
||||
|
||||
if not isinstance(api_key, str) or not api_key: # type: ignore
|
||||
raise ValueError("openai.api_key is not defined")
|
||||
|
||||
model = raw_config.get('model')
|
||||
|
||||
if not isinstance(model, str) or not model:
|
||||
raise ValueError("openai.model is not defined")
|
||||
|
||||
temperature = raw_config.get('temperature', 0.2)
|
||||
|
||||
if not isinstance(temperature, (int, float)):
|
||||
raise ValueError("openai.temperature is invalid")
|
||||
|
||||
top_p = raw_config.get('top_p', 1)
|
||||
|
||||
if not isinstance(top_p, (int, float)):
|
||||
raise ValueError("openai.top_p is invalid")
|
||||
|
||||
max_tokens = raw_config.get('max_tokens', 1024)
|
||||
|
||||
if not isinstance(max_tokens, (int)):
|
||||
raise ValueError("openai.max_tokens is invalid")
|
||||
|
||||
presence_penalty = raw_config.get('presence_penalty', 0)
|
||||
|
||||
if not isinstance(presence_penalty, (int, float)):
|
||||
raise ValueError("openai.presence_penalty is invalid")
|
||||
|
||||
frequency_penalty = raw_config.get('frequency_penalty', 0)
|
||||
|
||||
if not isinstance(frequency_penalty, (int, float)):
|
||||
raise ValueError("openai.frequency_penalty is invalid")
|
||||
|
||||
return Config(
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
max_tokens=max_tokens,
|
||||
presence_penalty=presence_penalty,
|
||||
frequency_penalty=presence_penalty,
|
||||
)
|
||||
|
||||
|
||||
def get_error_message(error: urllib.error.HTTPError) -> str:
|
||||
message = error.read().decode('utf-8', errors='ignore')
|
||||
|
||||
try:
|
||||
# JSON data might look like this:
|
||||
# {
|
||||
# "error": {
|
||||
# "message": "...",
|
||||
# "type": "...",
|
||||
# "param": null,
|
||||
# "code": null
|
||||
# }
|
||||
# }
|
||||
message = json.loads(message)['error']['message']
|
||||
|
||||
if "This model's maximum context length is" in message:
|
||||
message = 'Too much text for a request!'
|
||||
except Exception:
|
||||
# If we can't get a better message use the JSON payload at least.
|
||||
pass
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def main() -> None:
|
||||
input_data = json.loads(sys.stdin.readline())
|
||||
|
||||
try:
|
||||
config = load_config(input_data["config"])
|
||||
except ValueError as err:
|
||||
sys.exit(str(err))
|
||||
|
||||
try:
|
||||
get_openai_completion(config, input_data["prompt"])
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 400 or error.code == 401:
|
||||
message = get_error_message(error)
|
||||
sys.exit('Neural error: OpenAI request failure: ' + message)
|
||||
elif error.code == 429:
|
||||
sys.exit("Neural error: OpenAI request limit reached!")
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main() # pragma: no cover
|
||||
@@ -0,0 +1,96 @@
|
||||
# This enables: `pip install -e .`
|
||||
[tool.setuptools]
|
||||
# map the “root” package directory to src/
|
||||
package-dir = { "" = "src" }
|
||||
# turn on automatic package discovery
|
||||
packages = { find = { where = ["src"], include = ["*"] } }
|
||||
|
||||
[build-system]
|
||||
# NOTE: We should *not* add "wheel" here!
|
||||
# See: https://www.reddit.com/r/Python/comments/1jwbymm/psa_you_should_remove_wheel_from_your/
|
||||
requires = ["setuptools ~=77.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "neural"
|
||||
version = "0.2.0"
|
||||
description = "A coding agent"
|
||||
authors = [
|
||||
{name = "Georgi Angelchev", email = "george@denseanalysis.org"},
|
||||
{name = "Andrew Wray", email = "andrew@denseanalysis.org"},
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
# Core dependencies -> NONE
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyright==1.1.402",
|
||||
"pytest>=8.4.1",
|
||||
"ruff==0.10.0",
|
||||
"vim-vint===0.3.21",
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
venvPath = "./"
|
||||
venv = ".venv"
|
||||
pythonVersion = "3.10"
|
||||
extraPaths = ["src"]
|
||||
reportMissingImports = true
|
||||
reportMissingModuleSource = "error"
|
||||
typeCheckingMode = "strict"
|
||||
|
||||
# Settings for ruff
|
||||
[tool.ruff]
|
||||
line-length = 79
|
||||
target-version = "py310"
|
||||
|
||||
# Include all default rules from the ruff "all" baseline, but we'll fine-tune.
|
||||
lint.select = [
|
||||
"E", # PEP8 errors (confirms with PEP8)
|
||||
"W", # PEP8 warnings (confirms with PEP8)
|
||||
"I", # isort (sorts imports)
|
||||
"B", # flake8-bugbear rules (catches mulable by mistake and more)
|
||||
"A", # flake8-builtins (stop accidental shadowing)
|
||||
"COM", # flake8-commas (no accidental tuples and more)
|
||||
"ASYNC", # flake8-async rules (fixes async issues)
|
||||
"FBT", # flake8-boolean-trap rules (stops the Boolean Trap)
|
||||
"F", # Pyflakes (fixes common mistakes)
|
||||
"FAST", # FastAPI rules (fixes FastAPI misusage)
|
||||
"RUF", # ruff-specific rules (fixes common bugs)
|
||||
"UP", # pyupgrade (modernises code)
|
||||
"C90", # mccabe (no overly-complex code)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
# Flag errors (`C901`) whenever the complexity level exceeds 20
|
||||
# This is 20 possible branches through code in a single function.
|
||||
# See also: https://ntrs.nasa.gov/api/citations/20205011566/downloads/20205011566.pdf
|
||||
max-complexity = 20
|
||||
|
||||
# ruff isort settings
|
||||
[tool.ruff.lint.isort]
|
||||
# Black-compatible isort settings for ruff
|
||||
combine-as-imports = true
|
||||
force-wrap-aliases = true
|
||||
split-on-trailing-comma = true
|
||||
|
||||
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
|
||||
# You can keep third party libraries grouped together by adding them here.
|
||||
# known-third-party = []
|
||||
|
||||
# pytest configuration
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "7.0"
|
||||
addopts = "--strict-markers --tb=short"
|
||||
testpaths = ["test/python"]
|
||||
python_files = ["test_*.py"]
|
||||
markers = [
|
||||
"slow: mark slow tests",
|
||||
"integration: mark integration tests",
|
||||
"allow_network: allow network connections for this test",
|
||||
"unit: mark unit tests",
|
||||
]
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"venvPath": "./",
|
||||
"venv": "env",
|
||||
"pythonVersion": "3.10",
|
||||
"include": [],
|
||||
"exclude": [],
|
||||
"reportUnusedVariable": false,
|
||||
"reportUnusedClass": false,
|
||||
"reportUnusedFunction": false,
|
||||
"typeCheckingMode": "strict"
|
||||
}
|
||||
@@ -31,10 +31,6 @@ run_vim_90_tests=1
|
||||
run_linters=1
|
||||
run_python=1
|
||||
|
||||
# --no-docker can be set to disable docker wherever possible.
|
||||
# This doesn't apply to Vim at the moment, but we can skip the build stage.
|
||||
NO_DOCKER=0
|
||||
|
||||
while [ $# -ne 0 ]; do
|
||||
case $1 in
|
||||
-v)
|
||||
@@ -109,10 +105,6 @@ while [ $# -ne 0 ]; do
|
||||
run_python=1
|
||||
shift
|
||||
;;
|
||||
--no-docker)
|
||||
NO_DOCKER=1
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo 'Usage: ./run-tests [OPTION]... [FILE]...'
|
||||
echo
|
||||
@@ -131,7 +123,6 @@ while [ $# -ne 0 ]; do
|
||||
echo ' --linters-only Run only Vint and custom checks'
|
||||
echo ' --python-only Run only Python checks'
|
||||
echo ' --fast Run only the fastest Vim and the rest'
|
||||
echo ' --no-docker Run test scripts on host, except for Vim'
|
||||
echo ' --help Show this help text'
|
||||
echo ' -- Stop parsing options after this'
|
||||
exit 0
|
||||
@@ -150,8 +141,6 @@ while [ $# -ne 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
export NO_DOCKER
|
||||
|
||||
# Allow tests to be passed as arguments.
|
||||
if [ $# -ne 0 ]; then
|
||||
# This doesn't perfectly handle work splitting, but none of our files
|
||||
@@ -167,11 +156,9 @@ find test -name '*.swp' -delete
|
||||
|
||||
set -eu
|
||||
|
||||
if [ $NO_DOCKER -ne 1 ]; then
|
||||
# Check if docker un image is available locally
|
||||
has_image=$(docker images --quiet "${image}:${image_tag}" | wc -l)
|
||||
arch=$(docker info -f '{{ .Architecture }}')
|
||||
fi
|
||||
# Check if docker un image is available locally
|
||||
has_image=$(docker images --quiet "${image}:${image_tag}" | wc -l)
|
||||
arch=$(docker info -f '{{ .Architecture }}')
|
||||
|
||||
download_image() {
|
||||
if [[ $arch != x86_64 ]]; then
|
||||
@@ -182,7 +169,7 @@ download_image() {
|
||||
docker pull "${image}:${image_tag}" &> /dev/null
|
||||
}
|
||||
|
||||
if [ $NO_DOCKER -ne 1 ] && [ "$has_image" -eq 0 ] && ! download_image; then
|
||||
if [ "$has_image" -eq 0 ] && ! download_image; then
|
||||
echo "Building run image ${image}:${image_tag}"
|
||||
|
||||
build_args=( --build-arg GIT_VERSION="$git_version" )
|
||||
@@ -208,9 +195,7 @@ else
|
||||
echo "Docker run image ${image}:${image_tag} ready"
|
||||
fi
|
||||
|
||||
if [ $NO_DOCKER -ne 1 ]; then
|
||||
docker tag "${image}:${image_tag}" "${image}:latest"
|
||||
fi
|
||||
docker tag "${image}:${image_tag}" "${image}:latest"
|
||||
|
||||
output_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
|
||||
|
||||
@@ -221,8 +206,10 @@ pid_list=''
|
||||
|
||||
# Used for killing tests when you kill the script.
|
||||
cancel_tests() {
|
||||
# shellcheck disable=SC2317
|
||||
set +e
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
if [ -n "$pid_list" ]; then
|
||||
for pid in $pid_list; do
|
||||
kill "$pid"
|
||||
@@ -231,13 +218,17 @@ cancel_tests() {
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2046
|
||||
# shellcheck disable=SC2317
|
||||
docker kill $(docker ps -a -q --filter ancestor="$image" --format='{{.ID}}') &> /dev/null
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
if [ -d "$output_dir" ]; then
|
||||
rm -rf "$output_dir"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
echo
|
||||
# shellcheck disable=SC2317
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -270,7 +261,7 @@ if ((run_linters)); then
|
||||
|
||||
echo "Starting Custom checks..."
|
||||
file_number=$((file_number+1))
|
||||
test/script/custom-checks &> "$output_dir/$file_number" 2>&1 &
|
||||
test/script/run-custom-checks &> "$output_dir/$file_number" 2>&1 &
|
||||
pid_list="$pid_list $!"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
A Neural datasource for ChatGPT conversations.
|
||||
A Neural provider for GPT conversations.
|
||||
"""
|
||||
import json
|
||||
import platform
|
||||
@@ -7,9 +7,7 @@ import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
API_ENDPOINT = 'https://api.openai.com/v1/chat/completions'
|
||||
from typing import Any
|
||||
|
||||
OPENAI_DATA_HEADER = 'data: '
|
||||
OPENAI_DONE = '[DONE]'
|
||||
@@ -21,16 +19,21 @@ class Config:
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
url: str,
|
||||
api_key: str,
|
||||
model: str,
|
||||
use_chat_api: bool,
|
||||
temperature: float,
|
||||
top_p: float,
|
||||
max_tokens: int,
|
||||
presence_penalty: float,
|
||||
frequency_penalty: float,
|
||||
):
|
||||
self.url = url
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.use_chat_api = use_chat_api
|
||||
self.temperature = temperature
|
||||
self.top_p = top_p
|
||||
self.max_tokens = max_tokens
|
||||
@@ -38,21 +41,16 @@ class Config:
|
||||
self.frequency_penalty = frequency_penalty
|
||||
|
||||
|
||||
def get_chatgpt_completion(
|
||||
def get_openai_completion(
|
||||
config: Config,
|
||||
prompt: Union[str, List[Dict[str, str]]],
|
||||
prompt: str | list[dict[str, str]],
|
||||
) -> None:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {config.api_key}"
|
||||
"Authorization": f"Bearer {config.api_key}",
|
||||
}
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
"model": config.model,
|
||||
"messages": (
|
||||
[{"role": "user", "content": prompt}]
|
||||
if isinstance(prompt, str) else
|
||||
prompt
|
||||
),
|
||||
"temperature": config.temperature,
|
||||
"max_tokens": config.max_tokens,
|
||||
"top_p": 1,
|
||||
@@ -61,13 +59,26 @@ def get_chatgpt_completion(
|
||||
"stream": True,
|
||||
}
|
||||
|
||||
if config.use_chat_api:
|
||||
data["messages"] = (
|
||||
[{"role": "user", "content": prompt}]
|
||||
if isinstance(prompt, str) else
|
||||
prompt
|
||||
)
|
||||
else:
|
||||
data["prompt"] = prompt
|
||||
|
||||
req = urllib.request.Request(
|
||||
API_ENDPOINT,
|
||||
(
|
||||
f'{config.url}/v1/chat/completions'
|
||||
if config.use_chat_api else
|
||||
f'{config.url}/v1/completions'
|
||||
),
|
||||
data=json.dumps(data).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
role: Optional[str] = None
|
||||
role: str | None = None
|
||||
|
||||
# Disable SSL certificate verification on macOS.
|
||||
# This is bad for security, and we need to deal with SSL errors better.
|
||||
@@ -95,59 +106,94 @@ def get_chatgpt_completion(
|
||||
)
|
||||
|
||||
if line_data and line_data != OPENAI_DONE:
|
||||
delta = json.loads(line_data)["choices"][0]["delta"]
|
||||
# The role is typically in the first delta only.
|
||||
role = delta.get("role", role)
|
||||
openai_obj = json.loads(line_data)
|
||||
|
||||
if role == "assistant" and "content" in delta:
|
||||
print(delta["content"], end="", flush=True)
|
||||
if config.use_chat_api:
|
||||
delta = openai_obj["choices"][0]["delta"]
|
||||
# The role is typically in the first delta only.
|
||||
role = delta.get("role", role)
|
||||
|
||||
if role == "assistant" and "content" in delta:
|
||||
print(delta["content"], end="", flush=True)
|
||||
else:
|
||||
print(openai_obj["choices"][0]["text"], end="", flush=True)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def load_config(raw_config: Dict[str, Any]) -> Config:
|
||||
def load_config(raw_config: dict[str, Any]) -> Config:
|
||||
# TODO: Add range validation for request parameters.
|
||||
if not isinstance(raw_config, dict): # type: ignore
|
||||
raise ValueError("chatgpt config is not a dictionary")
|
||||
raise ValueError("openai config is not a dictionary")
|
||||
|
||||
url = raw_config.get('url')
|
||||
|
||||
if url is None:
|
||||
url = 'https://api.openai.com'
|
||||
elif not isinstance(url, str):
|
||||
raise ValueError("url must be a string")
|
||||
elif not url.startswith("http://") and not url.startswith("https://"):
|
||||
raise ValueError("url must start with http(s)://")
|
||||
|
||||
api_key = raw_config.get('api_key')
|
||||
|
||||
if not isinstance(api_key, str) or not api_key: # type: ignore
|
||||
raise ValueError("chatgpt.api_key is not defined")
|
||||
raise ValueError("api_key is not defined")
|
||||
|
||||
model = raw_config.get('model')
|
||||
|
||||
if not isinstance(model, str) or not model:
|
||||
raise ValueError("chatgpt.model is not defined")
|
||||
raise ValueError("model is not defined")
|
||||
|
||||
use_chat_api = raw_config.get('use_chat_api')
|
||||
|
||||
if use_chat_api is None:
|
||||
# Default to the older completions API if using certain older models.
|
||||
use_chat_api = model not in (
|
||||
'ada',
|
||||
'babbage',
|
||||
'curie',
|
||||
'davinci',
|
||||
'gpt-3.5-turbo-instruct',
|
||||
'text-ada-001',
|
||||
'text-babbage-001',
|
||||
'text-curie-001',
|
||||
'text-davinci-002',
|
||||
'text-davinci-003',
|
||||
)
|
||||
elif not isinstance(use_chat_api, bool):
|
||||
raise ValueError("use_chat_api must be true or false")
|
||||
|
||||
temperature = raw_config.get('temperature', 0.2)
|
||||
|
||||
if not isinstance(temperature, (int, float)):
|
||||
raise ValueError("chatgpt.temperature is invalid")
|
||||
if not isinstance(temperature, int | float):
|
||||
raise ValueError("temperature is invalid")
|
||||
|
||||
top_p = raw_config.get('top_p', 1)
|
||||
|
||||
if not isinstance(top_p, (int, float)):
|
||||
raise ValueError("chatgpt.top_p is invalid")
|
||||
if not isinstance(top_p, int | float):
|
||||
raise ValueError("top_p is invalid")
|
||||
|
||||
max_tokens = raw_config.get('max_tokens', 1024)
|
||||
|
||||
if not isinstance(max_tokens, (int)):
|
||||
raise ValueError("chatgpt.max_tokens is invalid")
|
||||
raise ValueError("max_tokens is invalid")
|
||||
|
||||
presence_penalty = raw_config.get('presence_penalty', 0)
|
||||
|
||||
if not isinstance(presence_penalty, (int, float)):
|
||||
raise ValueError("chatgpt.presence_penalty is invalid")
|
||||
if not isinstance(presence_penalty, int | float):
|
||||
raise ValueError("presence_penalty is invalid")
|
||||
|
||||
frequency_penalty = raw_config.get('frequency_penalty', 0)
|
||||
|
||||
if not isinstance(frequency_penalty, (int, float)):
|
||||
raise ValueError("chatgpt.frequency_penalty is invalid")
|
||||
if not isinstance(frequency_penalty, int | float):
|
||||
raise ValueError("frequency_penalty is invalid")
|
||||
|
||||
return Config(
|
||||
url=url,
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
use_chat_api=use_chat_api,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
max_tokens=max_tokens,
|
||||
@@ -189,13 +235,17 @@ def main() -> None:
|
||||
sys.exit(str(err))
|
||||
|
||||
try:
|
||||
get_chatgpt_completion(config, input_data["prompt"])
|
||||
get_openai_completion(config, input_data["prompt"])
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 400 or error.code == 401:
|
||||
message = get_error_message(error)
|
||||
sys.exit('Neural error: OpenAI request failure: ' + message)
|
||||
if error.code == 404:
|
||||
message = get_error_message(error)
|
||||
sys.exit('Neural error: OpenAI request failure: ' + message)
|
||||
elif error.code == 429:
|
||||
sys.exit("Neural error: OpenAI request limit reached!")
|
||||
message = get_error_message(error)
|
||||
sys.exit("Neural error: OpenAI request limit reached: " + message)
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
pyright==1.1.293
|
||||
pytest-cov==4.0.0
|
||||
pytest==7.2.1
|
||||
flake8==5.0.4
|
||||
flake8-isort==6.0.0
|
||||
isort==5.11.5
|
||||
@@ -3,18 +3,18 @@ import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from typing import Any, cast
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from neural_providers import chatgpt
|
||||
from neural.provider import openai
|
||||
|
||||
|
||||
def get_valid_config() -> Dict[str, Any]:
|
||||
def get_valid_config(model: str = "foo") -> dict[str, Any]:
|
||||
return {
|
||||
"api_key": ".",
|
||||
"model": "foo",
|
||||
"model": model,
|
||||
"prompt": "say hello",
|
||||
"temperature": 1,
|
||||
"top_p": 1,
|
||||
@@ -24,51 +24,91 @@ def get_valid_config() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def test_load_config_errors():
|
||||
def test_load_config_errors() -> None:
|
||||
with pytest.raises(ValueError) as exc:
|
||||
chatgpt.load_config(cast(Any, 0))
|
||||
openai.load_config(cast(Any, 0))
|
||||
|
||||
assert str(exc.value) == "chatgpt config is not a dictionary"
|
||||
assert str(exc.value) == "openai config is not a dictionary"
|
||||
|
||||
config: Dict[str, Any] = {}
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
for modification, expected_error in [
|
||||
({}, "chatgpt.api_key is not defined"),
|
||||
({"api_key": ""}, "chatgpt.api_key is not defined"),
|
||||
({"api_key": "."}, "chatgpt.model is not defined"),
|
||||
({"model": ""}, "chatgpt.model is not defined"),
|
||||
({"url": 1}, "url must be a string"),
|
||||
({"url": "x"}, "url must start with http(s)://"),
|
||||
({"url": "https://x", "api_key": ""}, "api_key is not defined"),
|
||||
({"api_key": "."}, "model is not defined"),
|
||||
({"model": ""}, "model is not defined"),
|
||||
(
|
||||
{"model": "x", "temperature": "x"},
|
||||
"chatgpt.temperature is invalid"
|
||||
{"model": "x", "use_chat_api": 1},
|
||||
"use_chat_api must be true or false",
|
||||
),
|
||||
(
|
||||
{"use_chat_api": None, "temperature": "x"},
|
||||
"temperature is invalid",
|
||||
),
|
||||
(
|
||||
{"temperature": 1, "top_p": "x"},
|
||||
"chatgpt.top_p is invalid"
|
||||
"top_p is invalid",
|
||||
),
|
||||
(
|
||||
{"top_p": 1, "max_tokens": "x"},
|
||||
"chatgpt.max_tokens is invalid"
|
||||
"max_tokens is invalid",
|
||||
),
|
||||
(
|
||||
{"max_tokens": 1, "presence_penalty": "x"},
|
||||
"chatgpt.presence_penalty is invalid"
|
||||
"presence_penalty is invalid",
|
||||
),
|
||||
(
|
||||
{"presence_penalty": 1, "frequency_penalty": "x"},
|
||||
"chatgpt.frequency_penalty is invalid"
|
||||
"frequency_penalty is invalid",
|
||||
),
|
||||
]:
|
||||
config.update(modification)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
chatgpt.load_config(config)
|
||||
openai.load_config(config)
|
||||
|
||||
assert str(exc.value) == expected_error, config
|
||||
|
||||
|
||||
def test_main_function_rate_other_error():
|
||||
def test_automatic_completions_api_usage() -> None:
|
||||
raw_config = get_valid_config()
|
||||
|
||||
for model in (
|
||||
'ada',
|
||||
'babbage',
|
||||
'curie',
|
||||
'davinci',
|
||||
'gpt-3.5-turbo-instruct',
|
||||
'text-ada-001',
|
||||
'text-babbage-001',
|
||||
'text-curie-001',
|
||||
'text-davinci-002',
|
||||
'text-davinci-003',
|
||||
):
|
||||
raw_config['model'] = model
|
||||
|
||||
assert openai.load_config(raw_config).use_chat_api is False
|
||||
|
||||
for model in ('gpt-3.5', 'gpt-4'):
|
||||
raw_config['model'] = model
|
||||
|
||||
assert openai.load_config(raw_config).use_chat_api is True
|
||||
|
||||
|
||||
def test_url_configuration() -> None:
|
||||
raw_config = get_valid_config()
|
||||
|
||||
assert openai.load_config(raw_config).url == 'https://api.openai.com'
|
||||
|
||||
raw_config['url'] = 'http://myhost'
|
||||
|
||||
assert openai.load_config(raw_config).url == 'http://myhost'
|
||||
|
||||
|
||||
def test_main_function_rate_other_error() -> None:
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(chatgpt, 'get_chatgpt_completion') as compl_mock:
|
||||
mock.patch.object(openai, 'get_openai_completion') as compl_mock:
|
||||
|
||||
compl_mock.side_effect = urllib.error.HTTPError(
|
||||
url='',
|
||||
@@ -83,10 +123,48 @@ def test_main_function_rate_other_error():
|
||||
})
|
||||
|
||||
with pytest.raises(urllib.error.HTTPError):
|
||||
chatgpt.main()
|
||||
openai.main()
|
||||
|
||||
|
||||
def test_print_chatgpt_results():
|
||||
def test_print_openai_completion_results() -> None:
|
||||
result_data = (
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "\\n", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "\\n", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "Hello", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "!", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "", "index": 0, "logprobs": null, "finish_reason": "stop"}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: [DONE]\n'
|
||||
b'\n'
|
||||
)
|
||||
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(urllib.request, 'urlopen') as urlopen_mock, \
|
||||
mock.patch('builtins.print') as print_mock:
|
||||
|
||||
urlopen_mock.return_value.__enter__.return_value = BytesIO(result_data)
|
||||
|
||||
readline_mock.return_value = json.dumps({
|
||||
"config": get_valid_config("gpt-3.5-turbo-instruct"),
|
||||
"prompt": "hello there",
|
||||
})
|
||||
openai.main()
|
||||
|
||||
assert print_mock.call_args_list == [
|
||||
mock.call('\n', end='', flush=True),
|
||||
mock.call('\n', end='', flush=True),
|
||||
mock.call('Hello', end='', flush=True),
|
||||
mock.call('!', end='', flush=True),
|
||||
mock.call('', end='', flush=True),
|
||||
mock.call(),
|
||||
]
|
||||
|
||||
|
||||
def test_print_openai_chat_completion_results() -> None:
|
||||
result_data = (
|
||||
b'data: {"id":"chatcmpl-6tMwjovREOTA84MkGBOS5rWyj1izv","object":"chat.completion.chunk","created":1678654265,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}\n' # noqa
|
||||
b'\n'
|
||||
@@ -115,10 +193,10 @@ def test_print_chatgpt_results():
|
||||
urlopen_mock.return_value.__enter__.return_value = BytesIO(result_data)
|
||||
|
||||
readline_mock.return_value = json.dumps({
|
||||
"config": get_valid_config(),
|
||||
"config": get_valid_config("gpt-3.5-turbo-0301"),
|
||||
"prompt": "Say this is a test",
|
||||
})
|
||||
chatgpt.main()
|
||||
openai.main()
|
||||
|
||||
assert print_mock.call_args_list == [
|
||||
mock.call('\n\n', end='', flush=True),
|
||||
@@ -131,15 +209,15 @@ def test_print_chatgpt_results():
|
||||
]
|
||||
|
||||
|
||||
def test_main_function_bad_config():
|
||||
def test_main_function_bad_config() -> None:
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(chatgpt, 'load_config') as load_config_mock:
|
||||
mock.patch.object(openai, 'load_config') as load_config_mock:
|
||||
|
||||
load_config_mock.side_effect = ValueError("expect this")
|
||||
readline_mock.return_value = json.dumps({"config": {}})
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
chatgpt.main()
|
||||
openai.main()
|
||||
|
||||
assert str(exc.value) == 'expect this'
|
||||
|
||||
@@ -149,8 +227,12 @@ def test_main_function_bad_config():
|
||||
(
|
||||
pytest.param(
|
||||
429,
|
||||
None,
|
||||
'OpenAI request limit reached!',
|
||||
json.dumps({
|
||||
'error': {
|
||||
'message': "Your token is bad",
|
||||
},
|
||||
}),
|
||||
'OpenAI request limit reached: Your token is bad',
|
||||
id="request_limit",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -185,15 +267,15 @@ def test_main_function_bad_config():
|
||||
'OpenAI request failure: Bad authentication error',
|
||||
id="unauthorised_failure",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_api_error(
|
||||
code: int,
|
||||
error_text: Optional[str],
|
||||
error_text: str | None,
|
||||
expected_message: str,
|
||||
):
|
||||
) -> None:
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(chatgpt, 'get_chatgpt_completion') as compl_mock:
|
||||
mock.patch.object(openai, 'get_openai_completion') as compl_mock:
|
||||
|
||||
compl_mock.side_effect = urllib.error.HTTPError(
|
||||
url='',
|
||||
@@ -209,6 +291,6 @@ def test_api_error(
|
||||
})
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
chatgpt.main()
|
||||
openai.main()
|
||||
|
||||
assert str(exc.value) == f'Neural error: {expected_message}'
|
||||
@@ -1,207 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from neural_providers import openai
|
||||
|
||||
|
||||
def get_valid_config() -> Dict[str, Any]:
|
||||
return {
|
||||
"api_key": ".",
|
||||
"model": "foo",
|
||||
"prompt": "say hello",
|
||||
"temperature": 1,
|
||||
"top_p": 1,
|
||||
"max_tokens": 1,
|
||||
"presence_penalty": 1,
|
||||
"frequency_penalty": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_load_config_errors():
|
||||
with pytest.raises(ValueError) as exc:
|
||||
openai.load_config(cast(Any, 0))
|
||||
|
||||
assert str(exc.value) == "openai config is not a dictionary"
|
||||
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
for modification, expected_error in [
|
||||
({}, "openai.api_key is not defined"),
|
||||
({"api_key": ""}, "openai.api_key is not defined"),
|
||||
({"api_key": "."}, "openai.model is not defined"),
|
||||
({"model": ""}, "openai.model is not defined"),
|
||||
(
|
||||
{"model": "x", "temperature": "x"},
|
||||
"openai.temperature is invalid"
|
||||
),
|
||||
(
|
||||
{"temperature": 1, "top_p": "x"},
|
||||
"openai.top_p is invalid"
|
||||
),
|
||||
(
|
||||
{"top_p": 1, "max_tokens": "x"},
|
||||
"openai.max_tokens is invalid"
|
||||
),
|
||||
(
|
||||
{"max_tokens": 1, "presence_penalty": "x"},
|
||||
"openai.presence_penalty is invalid"
|
||||
),
|
||||
(
|
||||
{"presence_penalty": 1, "frequency_penalty": "x"},
|
||||
"openai.frequency_penalty is invalid"
|
||||
),
|
||||
]:
|
||||
config.update(modification)
|
||||
|
||||
with pytest.raises(ValueError) as exc:
|
||||
openai.load_config(config)
|
||||
|
||||
assert str(exc.value) == expected_error, config
|
||||
|
||||
|
||||
def test_main_function_rate_other_error():
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(openai, 'get_openai_completion') as completion_mock:
|
||||
|
||||
completion_mock.side_effect = urllib.error.HTTPError(
|
||||
url='',
|
||||
msg='',
|
||||
hdrs=mock.Mock(),
|
||||
fp=None,
|
||||
code=500,
|
||||
)
|
||||
readline_mock.return_value = json.dumps({
|
||||
"config": get_valid_config(),
|
||||
"prompt": "hello there",
|
||||
})
|
||||
|
||||
with pytest.raises(urllib.error.HTTPError):
|
||||
openai.main()
|
||||
|
||||
|
||||
def test_print_openai_results():
|
||||
result_data = (
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "\\n", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "\\n", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "Hello", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "!", "index": 0, "logprobs": null, "finish_reason": null}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: {"id": "cmpl-6jMlRJtbYTGrNwE6Lxy1Ns1EtD0is", "object": "text_completion", "created": 1676270285, "choices": [{"text": "", "index": 0, "logprobs": null, "finish_reason": "stop"}], "model": "gpt-3.5-turbo-instruct"}\n' # noqa
|
||||
b'\n'
|
||||
b'data: [DONE]\n'
|
||||
b'\n'
|
||||
)
|
||||
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(urllib.request, 'urlopen') as urlopen_mock, \
|
||||
mock.patch('builtins.print') as print_mock:
|
||||
|
||||
urlopen_mock.return_value.__enter__.return_value = BytesIO(result_data)
|
||||
|
||||
readline_mock.return_value = json.dumps({
|
||||
"config": get_valid_config(),
|
||||
"prompt": "hello there",
|
||||
})
|
||||
openai.main()
|
||||
|
||||
assert print_mock.call_args_list == [
|
||||
mock.call('\n', end='', flush=True),
|
||||
mock.call('\n', end='', flush=True),
|
||||
mock.call('Hello', end='', flush=True),
|
||||
mock.call('!', end='', flush=True),
|
||||
mock.call('', end='', flush=True),
|
||||
mock.call(),
|
||||
]
|
||||
|
||||
|
||||
def test_main_function_bad_config():
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(openai, 'load_config') as load_config_mock:
|
||||
|
||||
load_config_mock.side_effect = ValueError("expect this")
|
||||
readline_mock.return_value = json.dumps({"config": {}})
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
openai.main()
|
||||
|
||||
assert str(exc.value) == 'expect this'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'code, error_text, expected_message',
|
||||
(
|
||||
pytest.param(
|
||||
429,
|
||||
None,
|
||||
'OpenAI request limit reached!',
|
||||
id="request_limit",
|
||||
),
|
||||
pytest.param(
|
||||
400,
|
||||
'{]',
|
||||
'OpenAI request failure: {]',
|
||||
id="error_with_mangled_json",
|
||||
),
|
||||
pytest.param(
|
||||
400,
|
||||
json.dumps({'error': {}}),
|
||||
'OpenAI request failure: {"error": {}}',
|
||||
id="error_with_missing_message_key",
|
||||
),
|
||||
pytest.param(
|
||||
400,
|
||||
json.dumps({
|
||||
'error': {
|
||||
'message': "This model's maximum context length is 123",
|
||||
},
|
||||
}),
|
||||
'OpenAI request failure: Too much text for a request!',
|
||||
id="too_much_text",
|
||||
),
|
||||
pytest.param(
|
||||
401,
|
||||
json.dumps({
|
||||
'error': {
|
||||
'message': "Bad authentication error",
|
||||
},
|
||||
}),
|
||||
'OpenAI request failure: Bad authentication error',
|
||||
id="unauthorised_failure",
|
||||
),
|
||||
)
|
||||
)
|
||||
def test_api_error(
|
||||
code: int,
|
||||
error_text: Optional[str],
|
||||
expected_message: str,
|
||||
):
|
||||
with mock.patch.object(sys.stdin, 'readline') as readline_mock, \
|
||||
mock.patch.object(openai, 'get_openai_completion') as compl_mock:
|
||||
|
||||
compl_mock.side_effect = urllib.error.HTTPError(
|
||||
url='',
|
||||
msg='',
|
||||
hdrs=mock.Mock(),
|
||||
fp=BytesIO(error_text.encode('utf-8')) if error_text else None,
|
||||
code=code,
|
||||
)
|
||||
|
||||
readline_mock.return_value = json.dumps({
|
||||
"config": get_valid_config(),
|
||||
"prompt": "hello there",
|
||||
})
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
openai.main()
|
||||
|
||||
assert str(exc.value) == f'Neural error: {expected_message}'
|
||||
@@ -5,8 +5,9 @@ particular Vim commands. This script ensures that VimL scripts are padded
|
||||
correctly, so they are easier to read.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
|
||||
INDENTATION_RE = re.compile(r'^ *')
|
||||
COMMENT_LINE_RE = re.compile(r'^ *"')
|
||||
@@ -25,18 +26,18 @@ WHITESPACE_FORBIDDEN_AFTER_SET = START_BLOCKS | MIDDLE_BLOCKS
|
||||
SAME_INDENTATION_SET = set(['\\'])
|
||||
|
||||
|
||||
def remove_comment_lines(line_iter):
|
||||
def remove_comments(line_iter: Iterator[str]) -> Iterator[tuple[int, str]]:
|
||||
for line_number, line in enumerate(line_iter, 1):
|
||||
if not COMMENT_LINE_RE.match(line):
|
||||
yield (line_number, line)
|
||||
|
||||
|
||||
def check_lines(line_iter):
|
||||
def check_lines(line_iter: Iterator[str]) -> Iterator[tuple[int, str]]:
|
||||
previous_indentation_level = None
|
||||
previous_command = None
|
||||
previous_line_blank = False
|
||||
|
||||
for line_number, line in remove_comment_lines(line_iter):
|
||||
for line_number, line in remove_comments(line_iter):
|
||||
if len(line) == 0:
|
||||
# Check for commands where we shouldn't have blank lines after
|
||||
# them, like `else` or the start of blocks like `function`.
|
||||
@@ -46,13 +47,14 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number,
|
||||
'Blank line forbidden after `%s`' % (previous_command,)
|
||||
f'Blank line forbidden after `{previous_command}`',
|
||||
)
|
||||
|
||||
previous_line_blank = True
|
||||
previous_command = None
|
||||
else:
|
||||
indentation_level = INDENTATION_RE.match(line).end()
|
||||
indent_match = INDENTATION_RE.match(line)
|
||||
indentation_level = indent_match.end() if indent_match else 4
|
||||
command_match = COMMAND_RE.match(line)
|
||||
|
||||
if command_match:
|
||||
@@ -65,7 +67,7 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number,
|
||||
'Line continuation should match previous indentation'
|
||||
'Line continuation should match previous indentation',
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -75,7 +77,7 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number,
|
||||
'Indentation should be 4 spaces'
|
||||
'Indentation should be 4 spaces',
|
||||
)
|
||||
|
||||
# Check for commands requiring blank lines before them, if they
|
||||
@@ -88,7 +90,7 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number,
|
||||
'Blank line required before `%s`' % (command,)
|
||||
f'Blank line required before `{command}`',
|
||||
)
|
||||
|
||||
# Check for commands where we shouldn't have blank lines before
|
||||
@@ -99,7 +101,7 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number - 1,
|
||||
'Blank line forbidden before `%s`' % (command,)
|
||||
f'Blank line forbidden before `{command}`',
|
||||
)
|
||||
|
||||
# Check for commands requiring blank lines after them, if they
|
||||
@@ -113,7 +115,7 @@ def check_lines(line_iter):
|
||||
):
|
||||
yield (
|
||||
line_number - 1,
|
||||
'Blank line required after `%s`' % (command,)
|
||||
f'Blank line required after `{command}`',
|
||||
)
|
||||
|
||||
previous_command = command
|
||||
@@ -123,11 +125,11 @@ def check_lines(line_iter):
|
||||
if OPERATOR_END_RE.search(line):
|
||||
yield (
|
||||
line_number,
|
||||
'Put operators at the start of lines instead'
|
||||
'Put operators at the start of lines instead',
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
status = 0
|
||||
|
||||
for filename in sys.argv[1:]:
|
||||
@@ -135,7 +137,7 @@ def main():
|
||||
line_iter = (line.rstrip() for line in vim_file)
|
||||
|
||||
for line_number, message in check_lines(line_iter):
|
||||
print('%s:%d %s' % (filename, line_number, message))
|
||||
print(f'{filename}:{line_number} {message}')
|
||||
status = 1
|
||||
|
||||
sys.exit(status)
|
||||
|
||||
@@ -58,11 +58,13 @@ section_index=0
|
||||
while read -r; do
|
||||
if [[ "$REPLY" =~ ^\ ]]; then
|
||||
if ! ((in_section)); then
|
||||
# shellcheck disable=SC2219
|
||||
let section_index='section_index + 1'
|
||||
in_section=1
|
||||
fi
|
||||
else
|
||||
if ((in_section)); then
|
||||
# shellcheck disable=SC2219
|
||||
let section_index='section_index + 1'
|
||||
in_section=0
|
||||
fi
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
||||
exit_code=0
|
||||
docker_flags=(--rm -v "$PWD:/testplugin" -v "$PWD/test:/home" -w /testplugin "$DOCKER_RUN_IMAGE")
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running custom linting rules'
|
||||
echo '========================================'
|
||||
echo 'Custom warnings/errors follow:'
|
||||
echo
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
docker run "${docker_flags[@]}" test/script/custom-linting-rules . || exit_code=$?
|
||||
else
|
||||
test/script/custom-linting-rules . || exit_code=$?
|
||||
fi
|
||||
|
||||
set +o pipefail
|
||||
|
||||
echo
|
||||
echo '========================================'
|
||||
echo 'Checking for duplicate tags'
|
||||
echo '========================================'
|
||||
echo 'Duplicate tags follow:'
|
||||
echo
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
docker run "${docker_flags[@]}" test/script/check-duplicate-tags . || exit_code=$?
|
||||
else
|
||||
test/script/check-duplicate-tags . || exit_code=$?
|
||||
fi
|
||||
|
||||
set +o pipefail
|
||||
|
||||
echo
|
||||
echo '========================================'
|
||||
echo 'Checking for invalid tag references'
|
||||
echo '========================================'
|
||||
echo 'Invalid tag references tags follow:'
|
||||
echo
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
docker run "${docker_flags[@]}" test/script/check-tag-references || exit_code=$?
|
||||
else
|
||||
test/script/check-tag-references || exit_code=$?
|
||||
fi
|
||||
|
||||
set +o pipefail
|
||||
|
||||
echo '========================================'
|
||||
echo 'Look for badly aligned doc tags'
|
||||
echo '========================================'
|
||||
echo 'Badly aligned tags follow:'
|
||||
echo
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
docker run "${docker_flags[@]}" test/script/check-tag-alignment || exit_code=$?
|
||||
else
|
||||
test/script/check-tag-alignment || exit_code=$?
|
||||
fi
|
||||
|
||||
set +o pipefail
|
||||
|
||||
exit $exit_code
|
||||
@@ -154,21 +154,8 @@ check_errors '\(!=.\?\|isnot\) type(\[\])' "Use 'isnot v:t_list' instead"
|
||||
check_errors '\(!=.\?\|isnot\) type({})' "Use 'isnot v:t_dict' instead"
|
||||
check_errors '\(!=.\?\|isnot\) type(function([^)]\+))' "Use 'isnot v:t_func' instead"
|
||||
|
||||
# Run a Python script to find lines that require padding around them. For
|
||||
# users without Python installed, we'll skip these checks. GitHub Actions will
|
||||
# run the script.
|
||||
python=''
|
||||
|
||||
if command -v python3.10 > /dev/null; then
|
||||
python=python3.10
|
||||
elif command -v python > /dev/null; then
|
||||
python=python
|
||||
fi
|
||||
|
||||
if [ -n "$python" ]; then
|
||||
if ! "$python" test/script/block-padding-checker "$directory"/**/*.vim; then
|
||||
exit_code=1
|
||||
fi
|
||||
if ! test/script/block-padding-checker "$directory"/**/*.vim; then
|
||||
exit_code=1
|
||||
fi
|
||||
|
||||
exit $exit_code
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
docker_flags=(--rm -w /testplugin -v "$PWD/test:/home")
|
||||
# Mount most directories, but definitely not .venv!
|
||||
for filename in *; do
|
||||
if \
|
||||
[ "$filename" != run-tests ] \
|
||||
&& [ "$filename" != Dockerfile ] \
|
||||
&& [[ "$filename" != *.md ]] \
|
||||
;then
|
||||
docker_flags+=("-v" "$PWD/$filename:/testplugin/$filename")
|
||||
fi
|
||||
done
|
||||
docker_flags+=("$DOCKER_RUN_IMAGE")
|
||||
# Use our compiled and installed Python executables,
|
||||
# and include the regular paths in PATH.
|
||||
docker_flags+=(env PATH='/root/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')
|
||||
|
||||
exit_code=0
|
||||
|
||||
set -o pipefail
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running custom linting rules'
|
||||
echo '========================================'
|
||||
echo 'Custom warnings/errors follow:'
|
||||
echo
|
||||
|
||||
docker run "${docker_flags[@]}" test/script/custom-linting-rules . || exit_code=$?
|
||||
|
||||
echo
|
||||
echo '========================================'
|
||||
echo 'Checking for duplicate tags'
|
||||
echo '========================================'
|
||||
echo 'Duplicate tags follow:'
|
||||
echo
|
||||
|
||||
docker run "${docker_flags[@]}" test/script/check-duplicate-tags . || exit_code=$?
|
||||
|
||||
echo
|
||||
echo '========================================'
|
||||
echo 'Checking for invalid tag references'
|
||||
echo '========================================'
|
||||
echo 'Invalid tag references tags follow:'
|
||||
echo
|
||||
|
||||
docker run "${docker_flags[@]}" test/script/check-tag-references || exit_code=$?
|
||||
|
||||
echo '========================================'
|
||||
echo 'Look for badly aligned doc tags'
|
||||
echo '========================================'
|
||||
echo 'Badly aligned tags follow:'
|
||||
echo
|
||||
|
||||
docker run "${docker_flags[@]}" test/script/check-tag-alignment || exit_code=$?
|
||||
|
||||
exit $exit_code
|
||||
+37
-25
@@ -2,35 +2,47 @@
|
||||
|
||||
set -eu
|
||||
|
||||
docker_flags=(--rm -w /testplugin -v "$PWD/test:/home")
|
||||
# Mount most directories, but definitely not .venv!
|
||||
for filename in *; do
|
||||
if \
|
||||
[ "$filename" != run-tests ] \
|
||||
&& [ "$filename" != Dockerfile ] \
|
||||
&& [[ "$filename" != *.md ]] \
|
||||
;then
|
||||
docker_flags+=("-v" "$PWD/$filename:/testplugin/$filename")
|
||||
fi
|
||||
done
|
||||
docker_flags+=("$DOCKER_RUN_IMAGE")
|
||||
# We can just use the virtualenv executables as PATH only, we only need those.
|
||||
# We can just set src only as PYTHONPATH, our code is there.
|
||||
docker_flags+=(env PATH=/root/.venv/bin PYTHONPATH=src)
|
||||
|
||||
exit_code=0
|
||||
|
||||
docker_flags=(--rm -v "$PWD:/testplugin" -w /testplugin "$DOCKER_RUN_IMAGE")
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running Python checks'
|
||||
echo '========================================'
|
||||
echo 'tox warnings/errors follow:'
|
||||
echo
|
||||
|
||||
# Use a different tox directory if running outside of docker to avoid issues
|
||||
# with the docker mounted directory writing as root, and other differences.
|
||||
if [ "$NO_DOCKER" -eq 1 ]; then
|
||||
tox_dir=.tox
|
||||
else
|
||||
tox_dir=.tox-docker
|
||||
fi
|
||||
|
||||
tox_command="tox --workdir $tox_dir --skip-missing-interpreters=false"
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
# shellcheck disable=2086
|
||||
docker run -a stdout "${docker_flags[@]}" $tox_command || exit_code=$?
|
||||
else
|
||||
$tox_command || exit_code=$?
|
||||
fi
|
||||
echo '========================================'
|
||||
echo 'Running pytest'
|
||||
echo '========================================'
|
||||
echo 'pytest warnings/errors follow:'
|
||||
echo
|
||||
|
||||
set +o pipefail
|
||||
docker run -a stdout "${docker_flags[@]}" pytest || exit_code=$?
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running pyright'
|
||||
echo '========================================'
|
||||
echo 'pyright warnings/errors follow:'
|
||||
echo
|
||||
|
||||
docker run -a stdout "${docker_flags[@]}" pyright || exit_code=$?
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running ruff'
|
||||
echo '========================================'
|
||||
echo 'ruff warnings/errors follow:'
|
||||
echo
|
||||
docker run -a stdout "${docker_flags[@]}" ruff check -q || exit_code=$?
|
||||
|
||||
exit $exit_code
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
set -eu
|
||||
|
||||
docker_flags=(--rm -w /testplugin -v "$PWD/test:/home")
|
||||
# Mount most directories, but definitely not .venv!
|
||||
for filename in *; do
|
||||
if \
|
||||
[ "$filename" != run-tests ] \
|
||||
&& [ "$filename" != Dockerfile ] \
|
||||
&& [[ "$filename" != *.md ]] \
|
||||
;then
|
||||
docker_flags+=("-v" "$PWD/$filename:/testplugin/$filename")
|
||||
fi
|
||||
done
|
||||
docker_flags+=("$DOCKER_RUN_IMAGE")
|
||||
|
||||
docker_flags=(--rm -v "$PWD:/testplugin" -v "$PWD/test:/home" -w /testplugin "$DOCKER_RUN_IMAGE")
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
nc='\033[0m'
|
||||
|
||||
+17
-18
@@ -3,10 +3,24 @@
|
||||
set -e
|
||||
set -u
|
||||
|
||||
required_version='0.3.21'
|
||||
docker_flags=(--rm -w /testplugin -v "$PWD/test:/home")
|
||||
# Mount most directories, but definitely not .venv!
|
||||
for filename in *; do
|
||||
if \
|
||||
[ "$filename" != run-tests ] \
|
||||
&& [ "$filename" != Dockerfile ] \
|
||||
&& [[ "$filename" != *.md ]] \
|
||||
;then
|
||||
docker_flags+=("-v" "$PWD/$filename:/testplugin/$filename")
|
||||
fi
|
||||
done
|
||||
docker_flags+=("$DOCKER_RUN_IMAGE")
|
||||
# We can just use the virtualenv executables as PATH only, we only need those.
|
||||
docker_flags+=(env PATH=/root/.venv/bin)
|
||||
|
||||
set -o pipefail
|
||||
|
||||
exit_code=0
|
||||
docker_flags=(--rm -v "$PWD:/testplugin" -v "$PWD/test:/home" -w /testplugin "$DOCKER_RUN_IMAGE")
|
||||
|
||||
echo '========================================'
|
||||
echo 'Running Vint to lint our code'
|
||||
@@ -14,22 +28,7 @@ echo '========================================'
|
||||
echo 'Vint warnings/errors follow:'
|
||||
echo
|
||||
|
||||
vint='/root/.pyenv/versions/3.10.10/bin/vint'
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ "$NO_DOCKER" -ne 1 ]; then
|
||||
docker run -a stdout "${docker_flags[@]}" "$vint" -s autoload plugin || exit_code=$?
|
||||
else
|
||||
if [ "$(vint --version)" != "$required_version" ]; then
|
||||
echo "You have have vint $(vint --version), but you need $required_version" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vint -s autoload plugin || exit_code=$?
|
||||
fi
|
||||
|
||||
set +o pipefail
|
||||
docker run -a stdout "${docker_flags[@]}" vint -s autoload plugin || exit_code=$?
|
||||
|
||||
echo
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Execute(Check that Neural scripts load at all):
|
||||
AssertEqual v:t_string, type(neural#Escape(''))
|
||||
AssertEqual v:t_dict, type(neural#source#openai#Get())
|
||||
AssertEqual v:t_dict, type(neural#provider#openai#Get())
|
||||
|
||||
+13
-14
@@ -21,27 +21,27 @@ After:
|
||||
|
||||
Restore
|
||||
|
||||
" Clean up any buffers run during tests.
|
||||
silent! bdelete! 'Neural Buffer'
|
||||
silent! bdelete! 'Test Neural Buffer Name'
|
||||
|
||||
Execute(It should create a neural buffer with default settings):
|
||||
NeuralBuffer
|
||||
|
||||
AssertEqual bufexists('Neural Buffer'), 1
|
||||
AssertEqual &filetype, 'neuralbuf'
|
||||
AssertEqual 1, bufexists('Neural Buffer')
|
||||
AssertEqual 'neuralbuf', &filetype
|
||||
" TODO: Assert if created with new or vertical new
|
||||
AssertEqual &l:wrap, 1
|
||||
AssertEqual &l:linebreak, 1
|
||||
|
||||
bdelete! Neural Buffer
|
||||
AssertEqual 1, &l:wrap
|
||||
AssertEqual 1, &l:linebreak
|
||||
|
||||
Execute(It should create a neural buffer with arguments):
|
||||
NeuralBuffer {"name": "Test Name", "create_mode": "horizontal", "wrap": v:false}
|
||||
NeuralBuffer {"name": "Test Neural Buffer Name", "create_mode": "horizontal", "wrap": v:false}
|
||||
|
||||
AssertEqual bufexists('Test Name'), 1
|
||||
AssertEqual &filetype, 'neuralbuf'
|
||||
AssertEqual 1, bufexists('Test Neural Buffer Name')
|
||||
AssertEqual 'neuralbuf', &filetype
|
||||
" TODO: Assert if created with new or vertical new
|
||||
AssertEqual &l:wrap, 0
|
||||
AssertEqual &l:linebreak, 0
|
||||
|
||||
bdelete! Test Name
|
||||
AssertEqual 0, &l:wrap
|
||||
AssertEqual 0, &l:linebreak
|
||||
|
||||
Given neuralbuf(A Neural buffer):
|
||||
write a story
|
||||
@@ -54,7 +54,6 @@ Execute(It should correctly run neural):
|
||||
\ ],
|
||||
\ g:calls
|
||||
|
||||
|
||||
" Plug mappings
|
||||
Execute(The correct neural buffer keybinds should be configured):
|
||||
redir => g:output
|
||||
|
||||
+77
-39
@@ -8,11 +8,6 @@ After:
|
||||
|
||||
Restore
|
||||
|
||||
Execute(The default selection should be correct):
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual 'openai', get(g:neural, 'selected')
|
||||
|
||||
Execute(The default keybind setting should be correct):
|
||||
call neural#config#Load()
|
||||
|
||||
@@ -37,34 +32,24 @@ Execute(The default echo settings should be correct):
|
||||
AssertEqual {'echo_enabled': v:true}, g:neural.ui
|
||||
|
||||
Execute(The default openai settings should be correct):
|
||||
let g:neural = {'providers': [{'type': 'openai'}]}
|
||||
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual
|
||||
\ {
|
||||
\ [{
|
||||
\ 'type': 'openai',
|
||||
\ 'url': 'https://api.openai.com',
|
||||
\ 'api_key': '',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 1024,
|
||||
\ 'model': 'gpt-3.5-turbo-instruct',
|
||||
\ 'use_chat_api': v:false,
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ },
|
||||
\ get(g:neural.source, 'openai')
|
||||
|
||||
Execute(The default chatgpt settings should be correct):
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual
|
||||
\ {
|
||||
\ 'api_key': '',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 2048,
|
||||
\ 'model': 'gpt-3.5-turbo',
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ },
|
||||
\ get(g:neural.source, 'chatgpt')
|
||||
\ }],
|
||||
\ get(g:neural, 'providers')
|
||||
|
||||
Execute(The default neural buffer settings should be correct):
|
||||
call neural#config#Load()
|
||||
@@ -85,9 +70,9 @@ Execute(Settings should be merged correctly):
|
||||
" Try the method of setting g:neural the first time.
|
||||
let g:neural = {
|
||||
\ 'ui': {'echo_enabled': v:true},
|
||||
\ 'source': {
|
||||
\ 'openai': {'api_key': 'mykey'},
|
||||
\ },
|
||||
\ 'providers': [
|
||||
\ {'type': 'openai', 'api_key': 'mykey'},
|
||||
\ ],
|
||||
\}
|
||||
else
|
||||
" Try calling Set with no global like Lua will the second time.
|
||||
@@ -96,17 +81,24 @@ Execute(Settings should be merged correctly):
|
||||
|
||||
" The results should be the same in each case.
|
||||
call neural#config#Load()
|
||||
call filter(g:neural, {key -> key =~ '\v^ui$|^source$'})
|
||||
call filter(g:neural, {key -> key =~ '\v^ui$|^providers$'})
|
||||
call filter(g:neural.ui, {key -> key is# 'echo_enabled'})
|
||||
call filter(g:neural.source, {key -> key is# 'openai'})
|
||||
call filter(g:neural.source.openai, {key -> key is# 'api_key'})
|
||||
|
||||
AssertEqual
|
||||
\ {
|
||||
\ 'ui': {'echo_enabled': v:true},
|
||||
\ 'source': {
|
||||
\ 'openai': {'api_key': 'mykey'},
|
||||
\ },
|
||||
\ 'providers': [{
|
||||
\ 'type': 'openai',
|
||||
\ 'url': 'https://api.openai.com',
|
||||
\ 'api_key': 'mykey',
|
||||
\ 'frequency_penalty': 0.1,
|
||||
\ 'max_tokens': 1024,
|
||||
\ 'model': 'gpt-3.5-turbo-instruct',
|
||||
\ 'use_chat_api': v:false,
|
||||
\ 'presence_penalty': 0.1,
|
||||
\ 'temperature': 0.2,
|
||||
\ 'top_p': 1,
|
||||
\ }],
|
||||
\ },
|
||||
\ g:neural
|
||||
endfor
|
||||
@@ -122,18 +114,64 @@ Execute(Settings should load quickly after the first merge):
|
||||
|
||||
Execute(Settings should update when g:neural is changed):
|
||||
call neural#config#Load()
|
||||
let g:neural = {'providers': [{'type': 'openai', 'api_key': 'mykey'}]}
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual 1, len(g:neural.providers)
|
||||
AssertEqual 'mykey', g:neural.providers[0].api_key
|
||||
AssertEqual 0.2, g:neural.providers[0].temperature
|
||||
|
||||
Execute(Settings migrated from an old configuration with a default selection should work):
|
||||
let g:neural = {'source': {'openai': {'api_key': 'mykey'}}}
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual 'mykey', g:neural.source.openai.api_key
|
||||
AssertEqual 0.2, g:neural.source.openai.temperature
|
||||
call map(g:neural.providers, {_, val -> filter(val, {key -> key =~ '\v^api_key$|^type|use_chat_api$'})})
|
||||
AssertEqual
|
||||
\ [{'type': 'openai', 'api_key': 'mykey', 'use_chat_api': v:false}],
|
||||
\ g:neural.providers
|
||||
Assert !has_key(g:neural, 'selected')
|
||||
Assert !has_key(g:neural, 'source')
|
||||
|
||||
Execute(The chatgpt api_key should default to the openai api_key):
|
||||
call neural#config#Set({
|
||||
\ 'ui': {'echo_enabled': v:true},
|
||||
Execute(Settings migrated from an old configuration with an openai selection should work):
|
||||
let g:neural = {
|
||||
\ 'source': {
|
||||
\ 'openai': {'api_key': 'mykey'},
|
||||
\ 'chatgpt': {'api_key': 'otherkey'}
|
||||
\ },
|
||||
\ 'selected': 'openai'
|
||||
\}
|
||||
call neural#config#Load()
|
||||
|
||||
call map(g:neural.providers, {_, val -> filter(val, {key -> key =~ '\v^api_key$|^type|use_chat_api$'})})
|
||||
AssertEqual
|
||||
\ [{'type': 'openai', 'api_key': 'mykey', 'use_chat_api': v:false}],
|
||||
\ g:neural.providers
|
||||
Assert !has_key(g:neural, 'selected')
|
||||
Assert !has_key(g:neural, 'source')
|
||||
|
||||
Execute(Settings migrated from an old configuration with chatgpt should work):
|
||||
let g:neural = {'source': {'chatgpt': {'api_key': 'mykey'}}, 'selected': 'chatgpt'}
|
||||
call neural#config#Load()
|
||||
|
||||
call map(g:neural.providers, {_, val -> filter(val, {key -> key =~ '\v^api_key$|^type|use_chat_api$'})})
|
||||
AssertEqual
|
||||
\ [{'type': 'chatgpt', 'api_key': 'mykey', 'use_chat_api': v:true}],
|
||||
\ g:neural.providers
|
||||
Assert !has_key(g:neural, 'selected')
|
||||
Assert !has_key(g:neural, 'source')
|
||||
|
||||
Execute(Settings migrated from an old configuration with chatgpt and the key set once for openai should work):
|
||||
let g:neural = {
|
||||
\ 'source': {
|
||||
\ 'openai': {'api_key': 'mykey'},
|
||||
\ },
|
||||
\})
|
||||
\ 'selected': 'chatgpt'
|
||||
\}
|
||||
call neural#config#Load()
|
||||
|
||||
AssertEqual 'mykey', g:neural.source.chatgpt.api_key
|
||||
call map(g:neural.providers, {_, val -> filter(val, {key -> key =~ '\v^api_key$|^type|use_chat_api$'})})
|
||||
AssertEqual
|
||||
\ [{'type': 'chatgpt', 'api_key': 'mykey', 'use_chat_api': v:true}],
|
||||
\ g:neural.providers
|
||||
Assert !has_key(g:neural, 'selected')
|
||||
Assert !has_key(g:neural, 'source')
|
||||
|
||||
@@ -91,7 +91,7 @@ Execute():
|
||||
\ 'neural#job#SendRaw',
|
||||
\ 1,
|
||||
\ json_encode({
|
||||
\ 'config': g:neural.source[g:neural.selected],
|
||||
\ 'config': g:neural.providers[0],
|
||||
\ 'prompt': "Explain these lines:\n\nne\nTwo\nThree\nFou",
|
||||
\ }) . "\n",
|
||||
\ ]
|
||||
|
||||
@@ -99,7 +99,7 @@ Execute(Running Neural and collecting output should work):
|
||||
\ 'neural#job#SendRaw',
|
||||
\ 1,
|
||||
\ json_encode({
|
||||
\ 'config': g:neural.source[g:neural.selected],
|
||||
\ 'config': g:neural.providers[0],
|
||||
\ 'prompt': 'this is a test',
|
||||
\ }) . "\n",
|
||||
\ ],
|
||||
|
||||
@@ -5,4 +5,4 @@ Execute(The OpenAI source Dictionary should be correct):
|
||||
\ 'script_language': 'python',
|
||||
\ 'script': neural#GetScriptDir() . '/openai.py'
|
||||
\ },
|
||||
\ neural#source#openai#Get()
|
||||
\ neural#provider#openai#Get()
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
[flake8]
|
||||
ignore = E125
|
||||
|
||||
[isort]
|
||||
known_first_party=neural_providers
|
||||
|
||||
[tox]
|
||||
requires = tox>=4
|
||||
env_list = lint, type, py{37,310}
|
||||
|
||||
[testenv]
|
||||
description = run unit tests
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
skip_install = true
|
||||
setenv = PYTHONPATH=.
|
||||
commands = pytest --cov=neural_providers --cov-fail-under=100 {posargs:test/python}
|
||||
|
||||
[testenv:lint]
|
||||
description = run linters
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
skip_install = true
|
||||
commands = flake8 {posargs:neural_providers test/python}
|
||||
|
||||
[testenv:type]
|
||||
description = run type checks
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
skip_install = true
|
||||
setenv = PYTHONPATH=.
|
||||
commands = pyright {postarg:neural_providers}
|
||||
@@ -0,0 +1,272 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "ansicolor"
|
||||
version = "0.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/74/630817c7eb1289a1412fcc4faeca74a69760d9c9b0db94fc09c91978a6ac/ansicolor-0.3.2.tar.gz", hash = "sha256:3b840a6b1184b5f1568635b1adab28147947522707d41ceba02d5ed0a0877279", size = 9725, upload-time = "2021-05-05T07:04:18.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/c7/acf0f7f9ac5e2da2989e570ddf5e4688101e91a90ae5ba3651955fa29fb8/ansicolor-0.3.2-py2.py3-none-any.whl", hash = "sha256:91e9fccea5cf596c39bc015d423ed2dd74c0104fd520a42d7acccb66cbfc39e9", size = 9793, upload-time = "2021-05-05T07:04:16.908Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neural"
|
||||
version = "0.2.0"
|
||||
source = { editable = "." }
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
{ name = "vim-vint" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pyright", specifier = "==1.1.402" },
|
||||
{ name = "pytest", specifier = ">=8.4.1" },
|
||||
{ name = "ruff", specifier = "==0.10.0" },
|
||||
{ name = "vim-vint", specifier = "===0.3.21" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.402"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload-time = "2025-06-11T08:48:35.759Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload-time = "2025-06-11T08:48:33.998Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/ec/9c59d2956566517c98ac8267554f4eaceafb2a19710a429368518b7fab43/ruff-0.10.0.tar.gz", hash = "sha256:fa1554e18deaf8aa097dbcfeafaf38b17a2a1e98fdc18f50e62e8a836abee392", size = 3789921, upload-time = "2025-03-13T18:38:05.228Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/3f/742afe91b43def2a75990b293c676355576c0ff9cdbcf4249f78fa592544/ruff-0.10.0-py3-none-linux_armv6l.whl", hash = "sha256:46a2aa0eaae5048e5f804f0be9489d8a661633e23277b7293089e70d5c1a35c4", size = 10078369, upload-time = "2025-03-13T18:37:20.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a0/8696fb4862e82f7b40bbbc2917137594b22826cc62d77278a91391507514/ruff-0.10.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:775a6bc61af9dd0a2e1763406522a137e62aabb743d8b43ed95f019cdd1526c7", size = 10876912, upload-time = "2025-03-13T18:37:24.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/aa/0d48b7b7d7a1f168bb8fd893ed559d633c7d68c4a8ef9b996f0c2bd07aca/ruff-0.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8b03e6fcd39d20f0004f9956f0ed5eadc404d3a299f9d9286323884e3b663730", size = 10229962, upload-time = "2025-03-13T18:37:28.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/de/861ced2f75b045d8cfc038d68961d8ac117344df1f43a11abdd05bf7991b/ruff-0.10.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621101d1af80248827f2409a78c8177c8319986a57b4663613b9c72f8617bfcd", size = 10404627, upload-time = "2025-03-13T18:37:30.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/69/666e0b840191c3ce433962c0d05fc0f6800afe259ea5d230cc731655d8e2/ruff-0.10.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2dfe85cb6bfbd4259801e7d4982f2a72bdbd5749dc73a09d68a6dbf77f2209a", size = 9939383, upload-time = "2025-03-13T18:37:33.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/bf/34a2adc58092c99cdfa9f1303acd82d840d56412022e477e2ab20c261d2d/ruff-0.10.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43ac3879a20c22fdc57e559f0bb27f0c71828656841d0b42d3505b1e5b3a83c8", size = 11492269, upload-time = "2025-03-13T18:37:35.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/3d/f7ccfcf69f15948623b190feea9d411d5029ae39725fcc078f8d43bd07a6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ef5e3aac421bbc62f8a7aab21edd49a359ed42205f7a5091a74386bca1efa293", size = 12186939, upload-time = "2025-03-13T18:37:38.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/3e/c557c0abfdea85c7d238a3cb238c73e7b6d17c30a584234c4fd8fe2cafb6/ruff-0.10.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f4f62d7fac8b748fce67ad308116b4d4cc1a9f964b4804fc5408fbd06e13ba9", size = 11655896, upload-time = "2025-03-13T18:37:40.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/8e/3bfa110f37e5192eb3943f14943d05fbb9a76fea380aa87655e6f6276a54/ruff-0.10.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9f6205c5b0d626f98da01a0e75b724a64c21c554bba24b12522c9e9ba6a04", size = 13885502, upload-time = "2025-03-13T18:37:43.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/4a/22cdab59b5563dd7f4c504d0f1e6bb25fc800a5a057395bc24f8ff3a85b2/ruff-0.10.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46a97f3d55f68464c48d1e929a8582c7e5bb80ac73336bbc7b0da894d8e6cd9e", size = 11344767, upload-time = "2025-03-13T18:37:46.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/0f/8f85de2ac565f82f47c6d8fb7ae04383e6300560f2d1b91c1268ff91e507/ruff-0.10.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0b811197d0dc96c13d610f8cfdc56030b405bcff5c2f10eab187b329da0ca4a", size = 10300331, upload-time = "2025-03-13T18:37:48.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4a/b337df327832cb30bd8607e8d1fdf1b6b5ca228307d5008dd49028fb66ae/ruff-0.10.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a13a3fda0870c1c964b47ff5d73805ae80d2a9de93ee2d185d453b8fddf85a84", size = 9926551, upload-time = "2025-03-13T18:37:50.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/e9/141233730b85675ac806c4b62f70516bd9c0aae8a55823f3a6589ed411be/ruff-0.10.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6ceb8d9f062e90ddcbad929f6136edf764bbf6411420a07e8357602ea28cd99f", size = 10925061, upload-time = "2025-03-13T18:37:53.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/09/02987935b55c2d353a226ac1b4f9718830e2e195834929f46c07eeede746/ruff-0.10.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c41d07d573617ed2f287ea892af2446fd8a8d877481e8e1ba6928e020665d240", size = 11394949, upload-time = "2025-03-13T18:37:55.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/ec/054f9879fb6f4122d43ffe5c9f88c8c323a9cd14220d5c813aea5805e02c/ruff-0.10.0-py3-none-win32.whl", hash = "sha256:76e2de0cbdd587e373cd3b4050d2c45babdd7014c1888a6f121c29525c748a15", size = 10272077, upload-time = "2025-03-13T18:37:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/49/915d8682f24645b904fe6a1aac36101464fc814923fdf293c1388dc5533c/ruff-0.10.0-py3-none-win_amd64.whl", hash = "sha256:f943acdecdcc6786a8d1dad455dd9f94e6d57ccc115be4993f9b52ef8316027a", size = 11393300, upload-time = "2025-03-13T18:38:00.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/ed/5c59941634c9026ceeccc7c119f23f4356f09aafd28c15c1bc734ac66b01/ruff-0.10.0-py3-none-win_arm64.whl", hash = "sha256:935a943bdbd9ff0685acd80d484ea91088e27617537b5f7ef8907187d19d28d0", size = 10510133, upload-time = "2025-03-13T18:38:02.91Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vim-vint"
|
||||
version = "0.3.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ansicolor" },
|
||||
{ name = "chardet" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/c7/d5fbe5f778edee83cba3aea8cc3308db327e4c161e0656e861b9cc2cb859/vim-vint-0.3.21.tar.gz", hash = "sha256:5dc59b2e5c2a746c88f5f51f3fafea3d639c6b0fdbb116bb74af27bf1c820d97", size = 65889, upload-time = "2019-05-13T23:05:01.995Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/57/b174313406c08fb5a4327e9f288ead04503c00d23c4f1765d88ca451977a/vim_vint-0.3.21-py2.py3-none-any.whl", hash = "sha256:61357ebbb41e909e194a4187419ad72bde9e67cc12eb13b31c578866b99d45c0", size = 89165, upload-time = "2019-05-13T23:05:00.001Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user