Fully convert Neural to use providers (#52)

This commit is contained in:
Andrew Wray
2025-07-12 15:51:50 +01:00
committed by GitHub
parent fcf5fdcd36
commit f43fa673d9
39 changed files with 1077 additions and 986 deletions
+3
View File
@@ -1,9 +1,12 @@
.coverage
.directory
.tox
*.pyc
.tox-docker
/env
__pycache__
tags
# pyenv
.python-version
*.pyc
*.egg-info
+1
View File
@@ -0,0 +1 @@
3.10.18
+25 -12
View File
@@ -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
+32 -10
View File
@@ -2,7 +2,9 @@
[![Vim](https://img.shields.io/badge/VIM-%2311AB00.svg?style=for-the-badge&logo=vim&logoColor=white)](https://www.vim.org/) [![Neovim](https://img.shields.io/badge/NeoVim-%2357A143.svg?&style=for-the-badge&logo=neovim&logoColor=white)](https://neovim.io/) [![CI](https://img.shields.io/github/actions/workflow/status/dense-analysis/neural/main.yml?branch=main&label=CI&logo=github&style=for-the-badge)](https://github.com/dense-analysis/neural/actions?query=event%3Apush+workflow%3ACI+branch%3Amain++) [![Join the Dense Analysis Discord server](https://img.shields.io/badge/chat-Discord-5865F2?style=for-the-badge&logo=appveyor)](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
View File
@@ -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
+8 -2
View File
@@ -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
View File
@@ -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
+3 -10
View File
@@ -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',
-10
View File
@@ -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
+8 -11
View File
@@ -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
View File
@@ -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
-16
View File
@@ -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
-194
View File
@@ -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
+96
View File
@@ -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",
]
-11
View File
@@ -1,11 +0,0 @@
{
"venvPath": "./",
"venv": "env",
"pythonVersion": "3.10",
"include": [],
"exclude": [],
"reportUnusedVariable": false,
"reportUnusedClass": false,
"reportUnusedFunction": false,
"typeCheckingMode": "strict"
}
+12 -21
View File
@@ -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
View File
@@ -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
-6
View File
@@ -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}'
-207
View File
@@ -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}'
+16 -14
View File
@@ -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)
+2
View File
@@ -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
-75
View File
@@ -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
+2 -15
View File
@@ -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
+59
View File
@@ -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
View File
@@ -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
+14 -3
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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')
+1 -1
View File
@@ -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",
\ ]
+1 -1
View File
@@ -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",
\ ],
+1 -1
View File
@@ -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()
-29
View File
@@ -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}
Generated
+272
View File
@@ -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" },
]