├── data └── .gitkeep ├── src └── surrealdb_openai │ ├── __init__.py │ ├── loggers.py │ ├── download.py │ ├── insert.py │ └── app.py ├── .env.example ├── .gitattributes ├── schema ├── define_ns_db.surql └── chats.surql ├── templates ├── create_chat.html ├── chats.html ├── send_system_message.html ├── send_user_message.html ├── load_chat.html └── index.html ├── LICENSE ├── pyproject.toml ├── static ├── surrealdb-icon.svg └── style.css ├── Makefile ├── README.md ├── .gitignore └── uv.lock /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/surrealdb_openai/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_TOKEN="" 2 | 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /schema/define_ns_db.surql: -------------------------------------------------------------------------------- 1 | DEFINE NAMESPACE IF NOT EXISTS test; 2 | USE NS test; 3 | DEFINE DATABASE IF NOT EXISTS test; 4 | 5 | -------------------------------------------------------------------------------- /templates/create_chat.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /templates/chats.html: -------------------------------------------------------------------------------- 1 | {% for chat in chats %} 2 | 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /templates/send_system_message.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | System 5 | {{ timestamp | convert_timestamp_to_date }} 6 |
7 |

{{ content | safe }}

8 |
9 | -------------------------------------------------------------------------------- /templates/send_user_message.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | User 5 | {{ timestamp | convert_timestamp_to_date }} 6 |
7 |

{{ content | safe }}

8 |
9 | 10 | -------------------------------------------------------------------------------- /templates/load_chat.html: -------------------------------------------------------------------------------- 1 |
2 | {% for message in messages %} 3 |
4 |
5 | {{ message.role | capitalize }} 6 | {{ message.timestamp | convert_timestamp_to_date }} 7 |
8 |

{{ message.content | safe }}

9 |
10 | {% endfor %} 11 |
12 | 13 |
14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/surrealdb_openai/loggers.py: -------------------------------------------------------------------------------- 1 | """Module to configure logging.""" 2 | 3 | import logging 4 | 5 | 6 | def setup_logger(name: str) -> logging.Logger: 7 | """Configure and return a logger with the given name. 8 | 9 | Args: 10 | name: Name of the logger. 11 | 12 | Returns: 13 | Configured Python logger. 14 | """ 15 | logger = logging.getLogger(name) 16 | logger.setLevel(logging.DEBUG) 17 | 18 | ch = logging.StreamHandler() 19 | ch.setLevel(logging.DEBUG) 20 | 21 | formatter = logging.Formatter( 22 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 23 | ) 24 | 25 | ch.setFormatter(formatter) 26 | logger.addHandler(ch) 27 | return logger 28 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ChatSurrealDB 9 | 10 | 11 | 12 | 13 | 14 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/surrealdb_openai/download.py: -------------------------------------------------------------------------------- 1 | """Download OpenAI Wikipedia data.""" 2 | 3 | import zipfile 4 | 5 | import wget 6 | 7 | from surrealdb_openai import loggers 8 | 9 | 10 | def download_data() -> None: 11 | """Extract `vector_database_wikipedia_articles_embedded.csv` to `/data`.""" 12 | logger = loggers.setup_logger("DownloadData") 13 | 14 | logger.info("Downloading Wikipedia") 15 | wget.download( 16 | url="https://cdn.openai.com/API/examples/data/" 17 | "vector_database_wikipedia_articles_embedded.zip", 18 | out="data/vector_database_wikipedia_articles_embedded.zip", 19 | ) 20 | 21 | logger.info("Extracting to data directory") 22 | with zipfile.ZipFile( 23 | "data/vector_database_wikipedia_articles_embedded.zip", "r" 24 | ) as zip_ref: 25 | zip_ref.extractall("data") 26 | 27 | logger.info("Extracted file successfully. Please check the data directory") 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Cellan Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "surrealdb_openai" 7 | version = "0" 8 | description = "Example of RAG using SurrealDB and OpenAI" 9 | authors = [ 10 | {name="Cellan Hall", email="cellan.hall@gmail.com"} 11 | ] 12 | readme = "README.md" 13 | license = {file = "LICENSE"} 14 | requires-python = ">=3.11, <3.12" 15 | dependencies = [ 16 | "pandas", 17 | "wget", 18 | "pandas-stubs", 19 | "surrealdb-beta @ git+https://github.com/surrealdb/surrealdb.py", 20 | "tqdm", 21 | "fastapi", 22 | "uvicorn", 23 | "jinja2", 24 | "python-multipart", 25 | "python-dotenv", 26 | ] 27 | 28 | [project.scripts] 29 | surreal-insert = "surrealdb_openai.insert:surreal_insert" 30 | download-data = "surrealdb_openai.download:download_data" 31 | 32 | [tool.ruff] 33 | target-version = "py311" 34 | line-length = 80 35 | 36 | [tool.ruff.lint] 37 | select = ["ALL"] 38 | ignore = ["ISC001", "COM812"] 39 | 40 | [tool.ruff.lint.pydocstyle] 41 | convention = "google" 42 | 43 | [tool.ruff.format] 44 | quote-style = "double" 45 | indent-style = "space" 46 | skip-magic-trailing-comma = false 47 | line-ending = "auto" 48 | 49 | [tool.uv] 50 | dev-dependencies = [ 51 | "ruff>=0.6.2", 52 | ] 53 | 54 | -------------------------------------------------------------------------------- /static/surrealdb-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | HOST := 0.0.0.0 3 | PORT := 8080 4 | ADDRESS := $(HOST):$(PORT) 5 | 6 | .PHONY: help 7 | # See for explanation 8 | help: 9 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 10 | 11 | .PHONY: fmt 12 | fmt: ## Format code 13 | ruff format . 14 | ruff check . --fix 15 | 16 | .PHONY: surreal-start 17 | surreal-start: ## Start SurrealDB 18 | surreal start --auth --user root --pass root --strict --log debug file://data/srdb.db --bind $(ADDRESS) -A 19 | 20 | .PHONY: surreal-init 21 | surreal-init: ## Initialise SurrealDB to populate with data 22 | surreal import --conn http://$(ADDRESS) --user root --pass root --ns test --db test schema/define_ns_db.surql 23 | surreal import --conn http://$(ADDRESS) --user root --pass root --ns test --db test schema/chats.surql 24 | 25 | .PHONY: surreal-remove 26 | surreal-remove: ## Remove the SurealDB database 27 | rm -rf data/srdb.db 28 | 29 | .PHONY: surreal-sql 30 | surreal-sql: ## Surreal SQL 31 | surreal sql -e ws://$(ADDRESS) --hide-welcome --pretty --json -u root -p root 32 | 33 | .PHONY: server-start 34 | server-start: ## Start FastAPI server 35 | uvicorn src.surrealdb_openai.app:app --reload 36 | 37 | .PHONY: pycache-remove 38 | pycache-remove: 39 | find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf 40 | 41 | .PHONY: dsstore-remove 42 | dsstore-remove: 43 | find . | grep -E ".DS_Store" | xargs rm -rf 44 | 45 | .PHONY: ruff-remove 46 | ruff-remove: 47 | find . | grep -E ".ruff_cache" | xargs rm -rf 48 | 49 | .PHONY: cleanup 50 | cleanup: pycache-remove dsstore-remove ruff-remove ## Cleanup residual files. 51 | -------------------------------------------------------------------------------- /src/surrealdb_openai/insert.py: -------------------------------------------------------------------------------- 1 | """Insert Wikipedia data into SurrealDB.""" 2 | 3 | import ast 4 | import string 5 | 6 | import pandas as pd 7 | import surrealdb 8 | import tqdm 9 | 10 | from surrealdb_openai import loggers 11 | 12 | FORMATTED_RECORD_FOR_INSERT_WIKI_EMBEDDING = string.Template( 13 | """{url: "$url", title: s"$title", text: s"$text", title_vector: $title_vector, content_vector: $content_vector}""" 14 | ) 15 | 16 | INSERT_WIKI_EMBEDDING_QUERY = string.Template( 17 | """ 18 | INSERT INTO wiki_embedding [\n $records\n]; 19 | """ 20 | ) 21 | 22 | TOTAL_ROWS = 25000 23 | CHUNK_SIZE = 100 24 | 25 | 26 | def surreal_insert() -> None: 27 | """Main entrypoint to insert Wikipedia embeddings into SurrealDB.""" 28 | logger = loggers.setup_logger("SurrealInsert") 29 | 30 | total_chunks = TOTAL_ROWS // CHUNK_SIZE + ( 31 | 1 if TOTAL_ROWS % CHUNK_SIZE else 0 32 | ) 33 | 34 | connection = surrealdb.SurrealDB("ws://localhost:8080/rpc") 35 | connection.signin(data={"username": "root", "password": "root"}) 36 | connection.use_namespace("test") 37 | connection.use_database("test") 38 | 39 | logger.info("Connected to SurrealDB") 40 | 41 | logger.info("Inserting rows into SurrealDB") 42 | with tqdm.tqdm(total=total_chunks, desc="Inserting") as pbar: 43 | for chunk in tqdm.tqdm( 44 | pd.read_csv( 45 | "data/vector_database_wikipedia_articles_embedded.csv", 46 | usecols=[ 47 | "url", 48 | "title", 49 | "text", 50 | "title_vector", 51 | "content_vector", 52 | ], 53 | chunksize=CHUNK_SIZE, 54 | ), 55 | ): 56 | formatted_rows = [ 57 | FORMATTED_RECORD_FOR_INSERT_WIKI_EMBEDDING.substitute( 58 | url=row["url"], 59 | title=row["title"] 60 | .replace("\\", "\\\\") 61 | .replace('"', '\\"'), 62 | text=row["text"].replace("\\", "\\\\").replace('"', '\\"'), 63 | title_vector=ast.literal_eval(row["title_vector"]), 64 | content_vector=ast.literal_eval(row["content_vector"]), 65 | ) 66 | for _, row in chunk.iterrows() 67 | ] 68 | connection.query( 69 | query=INSERT_WIKI_EMBEDDING_QUERY.substitute( 70 | records=",\n ".join(formatted_rows) 71 | ) 72 | ) 73 | pbar.update(1) 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SurrealDB x OpenAI: A Chat Playground 2 | 3 | Welcome to a project that explores the synergy between SurrealDB and OpenAI. 4 | 5 | https://github.com/Ce11an/surrealdb-openai/assets/60790416/a4e6a967-321a-4ca0-8f64-1687512aab38 6 | 7 | ## Project Overview 8 | 9 | This project aims to demonstrate the potential of integrating SurrealDB with OpenAI by importing 25,000 Wikipedia articles, complete with vector embeddings generated by OpenAI. The ultimate goal is to develop a Retrieval-Augmented Generation (RAG) question-answering system. 10 | 11 | The backend is powered by a FastAPI server, with Jinja2 handling template rendering and htmx enriching the frontend chat interface. 12 | 13 | ## Getting Started 14 | 15 | ### Prerequisites 16 | 17 | 1. **SurrealDB**: Ensure that SurrealDB is installed and configured on your machine. Installation instructions are available [here](https://surrealdb.com/install). 18 | 19 | 2. **Python 3.11**: We use [uv](https://docs.astral.sh/uv/) to manage dependencies. 20 | 21 | 3. **SurrealDB Python SDK**: The [SurrealDB Python SDK](https://github.com/surrealdb/surrealdb.py/tree/main) is currently in beta. To install the dependency, please ensure you have `rust` installed on your system. 22 | 23 | ### Installation 24 | 25 | 1. Clone the repository. 26 | 27 | 2. You will need an OpenAI API token for this project. If you don't have one, you can obtain it by following the [OpenAI Developer Quickstart](https://platform.openai.com/docs/quickstart). Rename the `.env.example` file to `.env` and replace the placeholder with your key. 28 | 29 | **Note:** This project is intended for experimental purposes only. Ensure that your OpenAI API key is kept secure. 30 | 31 | ### Setting Up SurrealDB 32 | 33 | To initialise SurrealDB, use the following `make` commands: 34 | 35 | 1. Start SurrealDB with on-disk persistence: 36 | 37 | ```bash 38 | make surreal-start 39 | ``` 40 | 41 | 2. Initialise the database with the required tables and functions: 42 | 43 | ```bash 44 | make surreal-init 45 | ``` 46 | 47 | 3. If you need to reset the database: 48 | 49 | ```bash 50 | make surreal-remove 51 | ``` 52 | 53 | ### Python Environment 54 | 55 | With `uv` installed, run: 56 | 57 | ```bash 58 | uv sync 59 | ``` 60 | 61 | Activate the Python virtual environment: 62 | 63 | ```bash 64 | source .venv/bin/activate 65 | ``` 66 | 67 | ### Downloading the Dataset 68 | 69 | Download the Simple English Wikipedia dataset, which includes vector embeddings (~700MB compressed, ~1.7GB CSV): 70 | 71 | ```bash 72 | download-data 73 | ``` 74 | 75 | ### Populating SurrealDB 76 | 77 | Insert the dataset into SurrealDB: 78 | 79 | ```bash 80 | surreal-insert 81 | ``` 82 | 83 | ### Starting the Chat Application 84 | 85 | To start the chat server: 86 | 87 | ```bash 88 | make server-start 89 | ``` 90 | 91 | ## Support 92 | 93 | If you find this project helpful, consider supporting the development: 94 | 95 | Buy Me A Coffee 96 | 97 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | font-family: sans-serif; 11 | display: flex; 12 | } 13 | 14 | nav { 15 | width: 300px; 16 | overflow-y: auto; 17 | padding: 20px; 18 | box-sizing: border-box; 19 | background: #000000; 20 | color: #f5f5f7; 21 | height: 100%; 22 | } 23 | 24 | button { 25 | background: #000000; 26 | color: #fff; 27 | border: 2px solid #000; 28 | padding: 10px 20px; 29 | border-radius: 8px; 30 | font-size: 16px; 31 | cursor: pointer; 32 | transition: background-color 0.3s ease; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | box-sizing: border-box; 36 | white-space: nowrap; 37 | margin-bottom: 10px; 38 | width: calc(100%); 39 | text-align: left; 40 | } 41 | 42 | nav button:last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | nav button:hover { 47 | background: #ff00a0; 48 | } 49 | 50 | button img.surrealdb-logo { 51 | height: 40px; 52 | padding-right: 10px; 53 | vertical-align: middle; 54 | } 55 | 56 | button img { 57 | -webkit-user-drag: none; 58 | } 59 | 60 | main { 61 | display: flex; 62 | flex-direction: column; 63 | width: 100%; 64 | height: 100%; 65 | padding: 20px; 66 | box-sizing: border-box; 67 | background: #151517; 68 | } 69 | 70 | main form { 71 | display: flex; 72 | height: 50px; 73 | } 74 | 75 | main form input { 76 | border-radius: 10px; 77 | border: 2px solid #444748; 78 | padding: 10px; 79 | box-sizing: border-box; 80 | font-family: inherit; 81 | margin-right: 10px; 82 | flex: 1; 83 | background: #151517; 84 | color: #f5f5f7; 85 | } 86 | 87 | main form button { 88 | width: 100px; 89 | padding: 10px; 90 | box-sizing: border-box; 91 | border-radius: 10px; 92 | background: #ff00a0; 93 | color: #fff; 94 | border: none; 95 | text-align: center; 96 | display: block; 97 | height: 100%; 98 | } 99 | 100 | a:-webkit-any-link { 101 | color: #ff00a0; 102 | cursor: pointer; 103 | text-decoration: underline; 104 | } 105 | 106 | .messages { 107 | overflow-y: auto; 108 | flex: 1; 109 | display: flex; 110 | flex-direction: column; 111 | padding: 10px; 112 | } 113 | 114 | .message { 115 | border-radius: 15px; 116 | padding: 15px; 117 | background: #151517; 118 | margin-bottom: 15px; 119 | color: #f5f5f7; 120 | display: flex; 121 | flex-direction: column; 122 | } 123 | 124 | .message .message-header { 125 | display: flex; 126 | justify-content: space-between; 127 | align-items: center; 128 | margin-bottom: 10px; 129 | } 130 | 131 | .message .messenger-name { 132 | font-weight: bold; 133 | font-size: 1rem; 134 | color: #ff00a0; 135 | } 136 | 137 | .message .message-time { 138 | font-size: 0.85rem; 139 | color: #f5f5f7; 140 | } 141 | 142 | .message p.message-content { 143 | margin: 0; 144 | } 145 | 146 | .system.message .messenger-name { 147 | color: #9600ff; 148 | } 149 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | # SurrealDB OpenAI 155 | data/* 156 | !data/.gitkeep 157 | history.txt 158 | .python-version 159 | 160 | -------------------------------------------------------------------------------- /src/surrealdb_openai/app.py: -------------------------------------------------------------------------------- 1 | """Backend for SurrealDB chat interface.""" 2 | 3 | import contextlib 4 | import datetime 5 | import os 6 | from collections.abc import AsyncGenerator 7 | 8 | import dotenv 9 | import fastapi 10 | import surrealdb 11 | from fastapi import responses, staticfiles, templating 12 | 13 | dotenv.load_dotenv() 14 | 15 | 16 | def extract_id(surrealdb_id: str) -> str: 17 | """Extract numeric ID from SurrealDB record ID. 18 | 19 | SurrealDB record ID comes in the form of `:`. 20 | CSS classes cannot be named with a `:` so for CSS we extract the ID. 21 | 22 | Args: 23 | surrealdb_id: SurrealDB record ID. 24 | 25 | Returns: 26 | ID. 27 | """ 28 | return surrealdb_id.split(":")[1] 29 | 30 | 31 | def convert_timestamp_to_date(timestamp: str) -> str: 32 | """Convert a SurrealDB `datetime` to a readable string. 33 | 34 | The result will be of the format: `April 05 2024, 15:30`. 35 | 36 | Args: 37 | timestamp: SurrealDB `datetime` value. 38 | 39 | Returns: 40 | Date as a string. 41 | """ 42 | parsed_timestamp = datetime.datetime.fromisoformat(timestamp.rstrip("Z")) 43 | return parsed_timestamp.strftime("%B %d %Y, %H:%M") 44 | 45 | 46 | templates = templating.Jinja2Templates(directory="templates") 47 | templates.env.filters["extract_id"] = extract_id 48 | templates.env.filters["convert_timestamp_to_date"] = convert_timestamp_to_date 49 | life_span = {} 50 | 51 | 52 | @contextlib.asynccontextmanager 53 | async def lifespan(_: fastapi.FastAPI) -> AsyncGenerator: 54 | """FastAPI lifespan to create and destroy objects.""" 55 | openai_token = os.environ["OPENAI_TOKEN"] 56 | 57 | connection = surrealdb.AsyncSurrealDB(url="ws://localhost:8080/rpc") 58 | await connection.connect() 59 | await connection.signin(data={"username": "root", "password": "root"}) 60 | await connection.use_namespace("test") 61 | await connection.use_database("test") 62 | await connection.set(key="openai_token", value=openai_token) 63 | life_span["surrealdb"] = connection 64 | yield 65 | life_span.clear() 66 | 67 | 68 | app = fastapi.FastAPI(lifespan=lifespan) 69 | app.mount("/static", staticfiles.StaticFiles(directory="static"), name="static") 70 | 71 | 72 | @app.get("/", response_class=responses.HTMLResponse) 73 | async def index(request: fastapi.Request) -> responses.HTMLResponse: 74 | return templates.TemplateResponse("index.html", {"request": request}) 75 | 76 | 77 | @app.post("/chats", response_class=responses.HTMLResponse) 78 | async def create_chat(request: fastapi.Request) -> responses.HTMLResponse: 79 | """Create a chat.""" 80 | chat_record = await life_span["surrealdb"].query( 81 | """RETURN fn::create_chat();""" 82 | ) 83 | return templates.TemplateResponse( 84 | "create_chat.html", 85 | { 86 | "request": request, 87 | "chat_id": chat_record.get("id"), 88 | "chat_title": chat_record.get("title"), 89 | }, 90 | ) 91 | 92 | 93 | @app.get("/chats/{chat_id}", response_class=responses.HTMLResponse) 94 | async def load_chat( 95 | request: fastapi.Request, chat_id: str 96 | ) -> responses.HTMLResponse: 97 | """Load a chat.""" 98 | message_records = await life_span["surrealdb"].query( 99 | f"""RETURN fn::load_chat({chat_id})""" 100 | ) 101 | return templates.TemplateResponse( 102 | "load_chat.html", 103 | { 104 | "request": request, 105 | "messages": message_records, 106 | "chat_id": chat_id, 107 | }, 108 | ) 109 | 110 | 111 | @app.get("/chats", response_class=responses.HTMLResponse) 112 | async def load_all_chats(request: fastapi.Request) -> responses.HTMLResponse: 113 | """Load all chats.""" 114 | chat_records = await life_span["surrealdb"].query( 115 | """RETURN fn::load_all_chats();""" 116 | ) 117 | return templates.TemplateResponse( 118 | "chats.html", {"request": request, "chats": chat_records} 119 | ) 120 | 121 | 122 | @app.post( 123 | "/chats/{chat_id}/send-user-message", response_class=responses.HTMLResponse 124 | ) 125 | async def send_user_message( 126 | request: fastapi.Request, 127 | chat_id: str, 128 | content: str = fastapi.Form(...), 129 | ) -> responses.HTMLResponse: 130 | """Send user message.""" 131 | message = await life_span["surrealdb"].query( 132 | f"""RETURN fn::create_user_message({chat_id}, s"{content}");""" 133 | ) 134 | return templates.TemplateResponse( 135 | "send_user_message.html", 136 | { 137 | "request": request, 138 | "chat_id": chat_id, 139 | "content": message.get("content"), 140 | "timestamp": message.get("timestamp"), 141 | }, 142 | ) 143 | 144 | 145 | @app.post( 146 | "/chats/{chat_id}/send-system-message", 147 | response_class=responses.HTMLResponse, 148 | ) 149 | async def send_system_message( 150 | request: fastapi.Request, chat_id: str 151 | ) -> responses.HTMLResponse: 152 | """Send system message.""" 153 | message = await life_span["surrealdb"].query( 154 | f"""RETURN fn::create_system_message({chat_id});""" 155 | ) 156 | 157 | title = await life_span["surrealdb"].query( 158 | f"""RETURN fn::get_chat_title({chat_id});""" 159 | ) 160 | 161 | return templates.TemplateResponse( 162 | "send_system_message.html", 163 | { 164 | "request": request, 165 | "content": message.get("content"), 166 | "timestamp": message.get("timestamp"), 167 | "create_title": title == "Untitled chat", 168 | "chat_id": chat_id, 169 | }, 170 | ) 171 | 172 | 173 | @app.post("/chats/{chat_id}/title", response_class=responses.PlainTextResponse) 174 | async def create_title(chat_id: str) -> responses.PlainTextResponse: 175 | """Create chat title.""" 176 | title = await life_span["surrealdb"].query( 177 | f"RETURN fn::generate_chat_title({chat_id});" 178 | ) 179 | return responses.PlainTextResponse(title.strip('"')) 180 | -------------------------------------------------------------------------------- /schema/chats.surql: -------------------------------------------------------------------------------- 1 | /* 2 | This file defines the SurrealQL for the chat functionality of this project. 3 | */ 4 | 5 | # We are required to specify the namespace and database we want our resources to reside in. 6 | USE NS test; USE DB test; 7 | 8 | # Define the `wiki_embedding` table. 9 | DEFINE TABLE IF NOT EXISTS wiki_embedding SCHEMAFULL; 10 | 11 | DEFINE FIELD IF NOT EXISTS url ON TABLE wiki_embedding TYPE string 12 | # Field must be a URL. 13 | ASSERT string::is::url($value); 14 | 15 | DEFINE FIELD IF NOT EXISTS title ON TABLE wiki_embedding TYPE string 16 | # Field must be non-empty 17 | ASSERT string::len($value) > 0; 18 | 19 | DEFINE FIELD IF NOT EXISTS text ON TABLE wiki_embedding TYPE string 20 | # Field must be non-empty 21 | ASSERT string::len($value) > 0; 22 | 23 | DEFINE FIELD IF NOT EXISTS title_vector ON TABLE wiki_embedding TYPE array 24 | # Field must have length 1536 to use embedding model: text-embedding-ada-002 25 | ASSERT array::len($value) = 1536; 26 | 27 | DEFINE FIELD IF NOT EXISTS content_vector ON TABLE wiki_embedding TYPE array 28 | # Field must have length 1536 to use embedding model: text-embedding-ada-002 29 | ASSERT array::len($value) = 1536; 30 | 31 | DEFINE INDEX IF NOT EXISTS wiki_embedding_content_vector_index ON wiki_embedding 32 | FIELDS content_vector 33 | MTREE DIMENSION 1536 34 | DIST COSINE; 35 | 36 | # Define the `chat` table. 37 | DEFINE TABLE IF NOT EXISTS chat SCHEMAFULL; 38 | 39 | DEFINE FIELD IF NOT EXISTS title ON TABLE chat TYPE string 40 | DEFAULT "Untitled chat"; 41 | 42 | # Field is populated on creation and is readonly. 43 | DEFINE FIELD IF NOT EXISTS created_at ON TABLE chat TYPE datetime 44 | VALUE time::now() READONLY; 45 | 46 | # Field automatically updates when a field is edited. 47 | DEFINE FIELD IF NOT EXISTS updated_at ON TABLE chat TYPE datetime 48 | VALUE time::now(); 49 | 50 | # Define the message table. 51 | DEFINE TABLE IF NOT EXISTS message SCHEMAFULL; 52 | 53 | /* Field can only be populated with `user` or `system`. 54 | 55 | There are CSS and HTML that relies on these values. 56 | */ 57 | DEFINE FIELD IF NOT EXISTS role ON message TYPE string 58 | ASSERT $input IN ["user", "system"]; 59 | 60 | DEFINE FIELD IF NOT EXISTS content ON message TYPE string; 61 | 62 | # Field is populated on creation and is readonly. 63 | DEFINE FIELD IF NOT EXISTS created_at ON TABLE message TYPE datetime 64 | VALUE time::now() READONLY; 65 | 66 | # Field automatically updates when a field is edited. 67 | DEFINE FIELD IF NOT EXISTS updated_at ON TABLE message TYPE datetime 68 | VALUE time::now(); 69 | 70 | # Define the `sent` edge table. 71 | DEFINE TABLE IF NOT EXISTS sent SCHEMAFULL; 72 | 73 | DEFINE FIELD IF NOT EXISTS in ON TABLE sent TYPE record; 74 | DEFINE FIELD IF NOT EXISTS out ON TABLE sent TYPE record; 75 | DEFINE FIELD IF NOT EXISTS timestamp ON TABLE sent TYPE datetime 76 | VALUE time::now(); 77 | 78 | # A message can only be sent in one chat 79 | DEFINE INDEX IF NOT EXISTS unique_sent_message_in_chat 80 | ON TABLE sent 81 | COLUMNS in, out UNIQUE; 82 | 83 | /* Get OpenAI token. 84 | 85 | Insert your token into this function before importing into SurrealDB. 86 | 87 | Returns: 88 | string: Bearer token. 89 | */ 90 | DEFINE FUNCTION IF NOT EXISTS fn::get_openai_token() { 91 | RETURN "Bearer " + $openai_token 92 | }; 93 | 94 | /* OpenAI embeddings complete. 95 | 96 | Args: 97 | embeddings_model: Embedding model from OpenAI. 98 | input: User input. 99 | 100 | Returns: 101 | array: Array of embeddings. 102 | */ 103 | DEFINE FUNCTION IF NOT EXISTS fn::embeddings_complete($embedding_model: string, $input: string) { 104 | RETURN http::post( 105 | "https://api.openai.com/v1/embeddings", 106 | { 107 | "model": $embedding_model, 108 | "input": $input 109 | }, 110 | { 111 | "Authorization": fn::get_openai_token() 112 | } 113 | )["data"][0]["embedding"] 114 | }; 115 | 116 | /* Search for documents using embeddings. 117 | 118 | Args: 119 | embeddings_model: Embedding model from OpenAI. 120 | input: User input. 121 | 122 | Returns: 123 | array: Array of embeddings. 124 | */ 125 | DEFINE FUNCTION IF NOT EXISTS fn::search_for_documents($input_vector: array, $threshold: float) { 126 | LET $context = ( 127 | SELECT 128 | url, 129 | title, 130 | text, 131 | vector::similarity::cosine(content_vector, $input_vector) AS similarity_score 132 | FROM wiki_embedding 133 | WHERE content_vector <|1|> $input_vector 134 | )[0]; 135 | 136 | RETURN IF $context.similarity_score > $threshold { 137 | type::string((SELECT url, title, text FROM $context)[0]) 138 | } ELSE { 139 | "" 140 | } 141 | }; 142 | 143 | 144 | /* Get prompt for RAG. 145 | 146 | Args: 147 | context: Context to add to the prompt. 148 | 149 | Returns: 150 | string: Prompt with context. 151 | */ 152 | DEFINE FUNCTION IF NOT EXISTS fn::get_prompt_with_context($context: string) { 153 | LET $prompt = "You are an AI assistant answering questions about anything from Simple English Wikipedia the context will provide you with the most relevant data from Simple English Wikipedia including the page title, url, and page content. 154 | 155 | If referencing the text/context refer to it as Simple English Wikipedia. 156 | 157 | Please provide your response in Markdown converted to HTML format. Include appropriate headings and lists where relevant. 158 | 159 | At the end of the response, add link a HTML link and replace the title and url with the associated title and url of the more relevant page from the context. 160 | 161 | The maximum number of links you can include is 1, do not provide any other references or annotations. 162 | 163 | Only reply with the context provided. If the context is an empty string, reply with 'I am sorry, I do not know the answer.'. 164 | 165 | Do not use any prior knowledge that you have been trained on. 166 | 167 | 168 | $context 169 | "; 170 | RETURN string::replace($prompt, '$context', $context); 171 | }; 172 | 173 | /* OpenAI chat complete. 174 | 175 | Args: 176 | llm: Large Language Model to use for generation. 177 | input: Initial user input. 178 | prompt_with_context: Prompt with context for the system. 179 | 180 | Returns: 181 | string: Response from LLM. 182 | */ 183 | DEFINE FUNCTION IF NOT EXISTS fn::chat_complete($llm: string, $input: string, $prompt_with_context: string, $temperature: float) { 184 | LET $response = http::post( 185 | "https://api.openai.com/v1/chat/completions", 186 | { 187 | "model": $llm, 188 | "messages": [ 189 | { 190 | "role": "system", 191 | "content": $prompt_with_context 192 | }, 193 | { 194 | "role": "user", "content": $input 195 | }, 196 | ], 197 | "temperature": $temperature 198 | }, 199 | { 200 | "Authorization": fn::get_openai_token() 201 | } 202 | )["choices"][0]["message"]["content"]; 203 | 204 | # Sometimes there are double quotes 205 | RETURN string::replace($response, '"', ''); 206 | }; 207 | 208 | /* Perfrom RAG using the `text-embedding-ada-002` model for embeddings. 209 | 210 | Args: 211 | llm: Large Language Model to use for generation. 212 | input: Initial user input. 213 | threshold: Similarity threshold to retreve documents. 214 | 215 | Returns: 216 | string: Response from LLM. 217 | */ 218 | DEFINE FUNCTION IF NOT EXISTS fn::surreal_rag($llm: string, $input: string, $threshold: float, $temperature: float) { 219 | LET $input_vector = fn::embeddings_complete("text-embedding-ada-002", $input); 220 | LET $context_document = fn::search_for_documents($input_vector, $threshold); 221 | LET $prompt_with_context = fn::get_prompt_with_context($context_document); 222 | RETURN fn::chat_complete($llm, $input, $prompt_with_context, $temperature); 223 | }; 224 | 225 | /* Create a message. 226 | 227 | Args: 228 | chat_id: Record ID from the `chat` table that the message was sent in. 229 | role: Role that sent the message. Allowed values are `user` or `system`. 230 | content: Sent message content. 231 | 232 | Returns: 233 | oject: Content and timestamp. 234 | */ 235 | DEFINE FUNCTION IF NOT EXISTS fn::create_message( 236 | $chat_id: record, 237 | $role: string, 238 | $content: string 239 | ) { 240 | # Create a message record and get the resulting ID. 241 | LET $message_id = 242 | SELECT VALUE 243 | id 244 | FROM ONLY 245 | CREATE ONLY message 246 | SET role = $role, 247 | content = $content; 248 | 249 | # Create a relation between the chat record and the message record and get the resulting timestamp. 250 | LET $timestamp = 251 | SELECT VALUE 252 | timestamp 253 | FROM ONLY 254 | RELATE ONLY $chat_id->sent->$message_id; 255 | 256 | RETURN { 257 | content: $content, 258 | timestamp: $timestamp 259 | }; 260 | }; 261 | 262 | /* Create a user message. 263 | 264 | Args: 265 | chat_id: Record ID from the `chat` table that the message was sent in. 266 | content: Sent message content. 267 | 268 | Returns: 269 | object: Content and timestamp. 270 | */ 271 | DEFINE FUNCTION IF NOT EXISTS fn::create_user_message($chat_id: record, $content: string) { 272 | RETURN fn::create_message($chat_id, "user", $content); 273 | }; 274 | 275 | /* Create system message using OpenAI. 276 | 277 | Args: 278 | chat_id: Record ID of the chat the user sent a message in. 279 | 280 | Returns: 281 | object: Content and timestamp. 282 | */ 283 | DEFINE FUNCTION IF NOT EXISTS fn::create_system_message($chat_id: record) { 284 | LET $last_user_message_content = ( 285 | SELECT 286 | out.content AS content, 287 | timestamp AS timestamp 288 | FROM ONLY $chat_id->sent 289 | ORDER BY timestamp DESC 290 | LIMIT 1 291 | FETCH out 292 | ).content; 293 | 294 | LET $llm_response = fn::surreal_rag("gpt-3.5-turbo", $last_user_message_content, 0.80, 0.5); 295 | 296 | RETURN fn::create_message($chat_id, "system", $llm_response); 297 | }; 298 | 299 | /* Generate a chat title based on the user's message in a chat. 300 | 301 | Args: 302 | chat_id: Record ID from the `chat` table to generate a title for. 303 | 304 | Returns: 305 | string: Generated chat title. 306 | */ 307 | DEFINE FUNCTION IF NOT EXISTS fn::generate_chat_title($chat_id: record) { 308 | # Get the `content` of the user's initial message. 309 | LET $first_message = ( 310 | SELECT 311 | out.content AS content, 312 | timestamp 313 | FROM ONLY $chat_id->sent 314 | ORDER BY timestamp 315 | LIMIT 1 316 | FETCH out 317 | ).content; 318 | 319 | # Define a system prompt to generate a title with. 320 | LET $system_prompt = "You are a conversation title generator for a ChatGPT type app. Respond only with a simple title using the user input."; 321 | 322 | # Use the Chat Complete API from OpenAI to generate the chat title. 323 | LET $generated_title = fn::chat_complete("gpt-3.5-turbo", $first_message, $system_prompt, 0.0); 324 | 325 | # Update the title of the chat and return the title. 326 | RETURN 327 | SELECT VALUE 328 | title 329 | FROM ONLY UPDATE ONLY $chat_id 330 | SET title = $generated_title 331 | RETURN title; 332 | }; 333 | 334 | /* Create a new chat. 335 | 336 | Returns: 337 | object: Object containing `id` and `title`. 338 | */ 339 | DEFINE FUNCTION IF NOT EXISTS fn::create_chat() { 340 | RETURN CREATE ONLY chat 341 | RETURN id, title; 342 | }; 343 | 344 | /* Load a chat. 345 | 346 | Args: 347 | chat_id: Record ID from the `chat` table to load. 348 | 349 | Returns: 350 | array[objects]: Array of messages containing `role` and `content`. 351 | */ 352 | DEFINE FUNCTION IF NOT EXISTS fn::load_chat($chat_id: record) { 353 | RETURN 354 | SELECT 355 | out.role AS role, 356 | out.content AS content, 357 | timestamp 358 | FROM $chat_id->sent 359 | ORDER BY timestamp 360 | FETCH out; 361 | }; 362 | 363 | /* Load all chats 364 | 365 | Returns: 366 | array[objects]: array of chats records containing `id`, `title`, and `created_at`. 367 | */ 368 | DEFINE FUNCTION IF NOT EXISTS fn::load_all_chats() { 369 | RETURN 370 | SELECT 371 | id, title, created_at 372 | FROM chat 373 | ORDER BY created_at DESC; 374 | }; 375 | 376 | /* Get chat title 377 | 378 | Args: Record ID of the chat to get the title for. 379 | 380 | Returns: 381 | string: Chat title. 382 | */ 383 | DEFINE FUNCTION IF NOT EXISTS fn::get_chat_title($chat_id: record) { 384 | RETURN SELECT VALUE title FROM ONLY $chat_id; 385 | }; 386 | 387 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = "==3.11.*" 3 | resolution-markers = [ 4 | "python_full_version < '3.12'", 5 | "python_full_version < '3.12'", 6 | "python_full_version == '3.12.*'", 7 | "python_full_version >= '3.13'", 8 | ] 9 | 10 | [[package]] 11 | name = "anyio" 12 | version = "4.4.0" 13 | source = { registry = "https://pypi.org/simple" } 14 | dependencies = [ 15 | { name = "idna" }, 16 | { name = "sniffio" }, 17 | ] 18 | sdist = { url = "https://files.pythonhosted.org/packages/e6/e3/c4c8d473d6780ef1853d630d581f70d655b4f8d7553c6997958c283039a2/anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", size = 163930 } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/7b/a2/10639a79341f6c019dedc95bd48a4928eed9f1d1197f4c04f546fc7ae0ff/anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7", size = 86780 }, 21 | ] 22 | 23 | [[package]] 24 | name = "click" 25 | version = "8.1.7" 26 | source = { registry = "https://pypi.org/simple" } 27 | dependencies = [ 28 | { name = "colorama", marker = "platform_system == 'Windows'" }, 29 | ] 30 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, 33 | ] 34 | 35 | [[package]] 36 | name = "colorama" 37 | version = "0.4.6" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 42 | ] 43 | 44 | [[package]] 45 | name = "fastapi" 46 | version = "0.112.2" 47 | source = { registry = "https://pypi.org/simple" } 48 | dependencies = [ 49 | { name = "pydantic" }, 50 | { name = "starlette" }, 51 | { name = "typing-extensions" }, 52 | ] 53 | sdist = { url = "https://files.pythonhosted.org/packages/9c/11/4874d165e7ef97aa803a567a4be8f9c8b0bd7cced6d536d44033ef7d4bfa/fastapi-0.112.2.tar.gz", hash = "sha256:3d4729c038414d5193840706907a41839d839523da6ed0c2811f1168cac1798c", size = 291967 } 54 | wheels = [ 55 | { url = "https://files.pythonhosted.org/packages/5c/ae/6570ae1b67007735229f10f2e1174d6e33c056ee9c0c1754d432acbede94/fastapi-0.112.2-py3-none-any.whl", hash = "sha256:db84b470bd0e2b1075942231e90e3577e12a903c4dc8696f0d206a7904a7af1c", size = 93471 }, 56 | ] 57 | 58 | [[package]] 59 | name = "h11" 60 | version = "0.14.0" 61 | source = { registry = "https://pypi.org/simple" } 62 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 63 | wheels = [ 64 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 65 | ] 66 | 67 | [[package]] 68 | name = "idna" 69 | version = "3.8" 70 | source = { registry = "https://pypi.org/simple" } 71 | sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/e349c5e6d4543326c6883ee9491e3921e0d07b55fdf3cce184b40d63e72a/idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603", size = 189467 } 72 | wheels = [ 73 | { url = "https://files.pythonhosted.org/packages/22/7e/d71db821f177828df9dea8c42ac46473366f191be53080e552e628aad991/idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", size = 66894 }, 74 | ] 75 | 76 | [[package]] 77 | name = "jinja2" 78 | version = "3.1.4" 79 | source = { registry = "https://pypi.org/simple" } 80 | dependencies = [ 81 | { name = "markupsafe" }, 82 | ] 83 | sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } 84 | wheels = [ 85 | { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, 86 | ] 87 | 88 | [[package]] 89 | name = "markupsafe" 90 | version = "2.1.5" 91 | source = { registry = "https://pypi.org/simple" } 92 | sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } 93 | wheels = [ 94 | { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, 95 | { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, 96 | { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, 97 | { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, 98 | { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, 99 | { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, 100 | { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, 101 | { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, 102 | { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, 103 | { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, 104 | { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, 105 | { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, 106 | { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, 107 | { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, 108 | { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, 109 | { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, 110 | { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, 111 | { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, 112 | { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, 113 | { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, 114 | ] 115 | 116 | [[package]] 117 | name = "numpy" 118 | version = "2.1.0" 119 | source = { registry = "https://pypi.org/simple" } 120 | sdist = { url = "https://files.pythonhosted.org/packages/54/a4/f8188c4f3e07f7737683588210c073478abcb542048cf4ab6fedad0b458a/numpy-2.1.0.tar.gz", hash = "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2", size = 18868922 } 121 | wheels = [ 122 | { url = "https://files.pythonhosted.org/packages/3e/98/466ac2a77706699ca0141ea197e4f221d2b232051052f8f794a628a489ec/numpy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce", size = 21153408 }, 123 | { url = "https://files.pythonhosted.org/packages/d5/43/4ff735420b31cd454e4b3acdd0ba7570b453aede6fa16cf7a11cc8780d1b/numpy-2.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1", size = 5350253 }, 124 | { url = "https://files.pythonhosted.org/packages/ec/a0/1c1b9d935d7196c4a847b76c8a8d012c986ddbc78ef159cc4c0393148062/numpy-2.1.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e", size = 6889274 }, 125 | { url = "https://files.pythonhosted.org/packages/d0/d2/4838d8c3b7ac69947ffd686ba3376cb603ea3618305ae3b8547b821df218/numpy-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb", size = 13982862 }, 126 | { url = "https://files.pythonhosted.org/packages/7b/93/831b4c5b4355210827b3de34f539297e1833c39a68c26a8b454d8cf9f5ed/numpy-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3", size = 16336222 }, 127 | { url = "https://files.pythonhosted.org/packages/db/44/7d2f454309a620f1afdde44dffa469fece331b84e7a5bd2dba3f0f465489/numpy-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36", size = 16708990 }, 128 | { url = "https://files.pythonhosted.org/packages/65/6b/46f69972a25e3b682b7a65cb525efa3650cd62e237180c2ecff7a6177173/numpy-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd", size = 14487554 }, 129 | { url = "https://files.pythonhosted.org/packages/3f/bc/4b128b3ac152e64e3d117931167bc2289dab47204762ad65011b681d75e7/numpy-2.1.0-cp311-cp311-win32.whl", hash = "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e", size = 6531834 }, 130 | { url = "https://files.pythonhosted.org/packages/7b/5e/093592740805fe401ce49a627cc8a3f034dac62b34d68ab69db3c56bd662/numpy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b", size = 12869011 }, 131 | { url = "https://files.pythonhosted.org/packages/eb/f5/a06a231cbeea4aff841ff744a12e4bf4d4407f2c753d13ce4563aa126c90/numpy-2.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736", size = 20882951 }, 132 | { url = "https://files.pythonhosted.org/packages/70/1d/4ad38e3a1840f72c29595c06b103ecd9119f260e897ff7e88a74adb0ca14/numpy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529", size = 13491878 }, 133 | { url = "https://files.pythonhosted.org/packages/b4/3b/569055d01ed80634d6be6ceef8fb28eb0866e4f98c2d97667dcf9fae3e22/numpy-2.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1", size = 5087346 }, 134 | { url = "https://files.pythonhosted.org/packages/24/37/212dd6fbd298c467b80d4d6217b2bc902b520e96a967b59f72603bf1142f/numpy-2.1.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8", size = 6618269 }, 135 | { url = "https://files.pythonhosted.org/packages/33/4d/435c143c06e16c8bfccbfd9af252b0a8ac7897e0c0e36e539d75a75e91b4/numpy-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745", size = 13695244 }, 136 | { url = "https://files.pythonhosted.org/packages/48/3e/bf807eb050abc23adc556f34fcf931ca2d67ad8dfc9c17fcd9332c01347f/numpy-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111", size = 16040181 }, 137 | { url = "https://files.pythonhosted.org/packages/cd/a9/40dc96b5d43076836d82d1e84a3a4a6a4c2925a53ec0b7f31271434ff02c/numpy-2.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0", size = 16407920 }, 138 | { url = "https://files.pythonhosted.org/packages/cc/77/39e44cf0a6eb0f93b18ffb00f1964b2c471b1df5605aee486c221b06a8e4/numpy-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574", size = 14170943 }, 139 | { url = "https://files.pythonhosted.org/packages/54/02/f0a3c2ec1622dc4346bd126e2578948c7192b3838c893a3d215738fb367b/numpy-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02", size = 6235947 }, 140 | { url = "https://files.pythonhosted.org/packages/8c/bf/d9d214a9dff020ad1663f1536f45d34e052e4c7f630c46cd363e785e3231/numpy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc", size = 12566546 }, 141 | { url = "https://files.pythonhosted.org/packages/c3/16/6b536e1b67624178e3631a3fa60c9c1b5ee7cda2fa9492c4f2de01bfcb06/numpy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667", size = 20833354 }, 142 | { url = "https://files.pythonhosted.org/packages/52/87/130e95aa8a6383fc3de4fdaf7adc629289b79b88548fb6e35e9d924697d7/numpy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e", size = 13506169 }, 143 | { url = "https://files.pythonhosted.org/packages/d9/c2/0fcf68c67681f9ad9d76156b4606f60b48748ead76d4ba19b90aecd4b626/numpy-2.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33", size = 5072908 }, 144 | { url = "https://files.pythonhosted.org/packages/72/40/e21bbbfae665ef5fa1dfd7eae1c5dc93ba9d3b36e39d2d38789dd8c22d56/numpy-2.1.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b", size = 6604906 }, 145 | { url = "https://files.pythonhosted.org/packages/0e/ce/848967516bf8dd4f769886a883a4852dbc62e9b63b1137d2b9900f595222/numpy-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195", size = 13690864 }, 146 | { url = "https://files.pythonhosted.org/packages/15/72/2cebe04758e1123f625ed3221cb3c48602175ad619dd9b47de69689b4656/numpy-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977", size = 16036272 }, 147 | { url = "https://files.pythonhosted.org/packages/a7/b7/ae34ced7864b551e0ea01ce4e7acbe7ddf5946afb623dea39760b19bc8b0/numpy-2.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1", size = 16408978 }, 148 | { url = "https://files.pythonhosted.org/packages/4d/22/c9d696b87c5ce25e857d7745fe4f090373a2daf8c26f5e15b32b5db7bff7/numpy-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62", size = 14168398 }, 149 | { url = "https://files.pythonhosted.org/packages/9e/8b/63f74dccf86d4832d593bdbe06544f4a0a1b7e18e86e0db1e8231bf47c49/numpy-2.1.0-cp313-cp313-win32.whl", hash = "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324", size = 6232743 }, 150 | { url = "https://files.pythonhosted.org/packages/23/4b/e30a3132478c69df3e3e587fa87dcbf2660455daec92d8d52e7028a92554/numpy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5", size = 12560212 }, 151 | { url = "https://files.pythonhosted.org/packages/5a/1b/40e881a3a272c4861de1e43a3e7ee1559988dd12187463726d3b395a8874/numpy-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06", size = 20840821 }, 152 | { url = "https://files.pythonhosted.org/packages/d0/8e/5b7c08f9238f6cc18037f6fd92f83feaa8c19e9decb6bd075cad81f71fae/numpy-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883", size = 13500478 }, 153 | { url = "https://files.pythonhosted.org/packages/65/32/bf9df25ef50761fcb3e089c745d2e195b35cc6506d032f12bb5cc28f6c43/numpy-2.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300", size = 5095825 }, 154 | { url = "https://files.pythonhosted.org/packages/50/34/d18c95bc5981ea3bb8e6f896aad12159a37dcc67b22cd9464fe3899612f7/numpy-2.1.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d", size = 6611470 }, 155 | { url = "https://files.pythonhosted.org/packages/b4/4f/27d56e9f6222419951bfeef54bc0a71dc40c0ebeb248e1aa85655da6fa11/numpy-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd", size = 13647061 }, 156 | { url = "https://files.pythonhosted.org/packages/f9/e0/ae6e12a157c4ab415b380d0f3596cb9090a0c4acf48cd8cd7bc6d6b93d24/numpy-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6", size = 16006479 }, 157 | { url = "https://files.pythonhosted.org/packages/ab/da/b746668c7303bd73af262208abbfa8b1c86be12e9eccb0d3021ed8a58873/numpy-2.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a", size = 16383064 }, 158 | { url = "https://files.pythonhosted.org/packages/f4/51/c0dcadea0c281be5db32b29f7b977b17bdb53b7dbfcbc3b4f49288de8696/numpy-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb", size = 14135556 }, 159 | ] 160 | 161 | [[package]] 162 | name = "pandas" 163 | version = "2.2.2" 164 | source = { registry = "https://pypi.org/simple" } 165 | dependencies = [ 166 | { name = "numpy" }, 167 | { name = "python-dateutil" }, 168 | { name = "pytz" }, 169 | { name = "tzdata" }, 170 | ] 171 | sdist = { url = "https://files.pythonhosted.org/packages/88/d9/ecf715f34c73ccb1d8ceb82fc01cd1028a65a5f6dbc57bfa6ea155119058/pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", size = 4398391 } 172 | wheels = [ 173 | { url = "https://files.pythonhosted.org/packages/1b/70/61704497903d43043e288017cb2b82155c0d41e15f5c17807920877b45c2/pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", size = 12574808 }, 174 | { url = "https://files.pythonhosted.org/packages/16/c6/75231fd47afd6b3f89011e7077f1a3958441264aca7ae9ff596e3276a5d0/pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", size = 11304876 }, 175 | { url = "https://files.pythonhosted.org/packages/97/2d/7b54f80b93379ff94afb3bd9b0cd1d17b48183a0d6f98045bc01ce1e06a7/pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", size = 15602548 }, 176 | { url = "https://files.pythonhosted.org/packages/fc/a5/4d82be566f069d7a9a702dcdf6f9106df0e0b042e738043c0cc7ddd7e3f6/pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", size = 13031332 }, 177 | { url = "https://files.pythonhosted.org/packages/92/a2/b79c48f530673567805e607712b29814b47dcaf0d167e87145eb4b0118c6/pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", size = 16286054 }, 178 | { url = "https://files.pythonhosted.org/packages/40/c7/47e94907f1d8fdb4868d61bd6c93d57b3784a964d52691b77ebfdb062842/pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", size = 13879507 }, 179 | { url = "https://files.pythonhosted.org/packages/ab/63/966db1321a0ad55df1d1fe51505d2cdae191b84c907974873817b0a6e849/pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", size = 11634249 }, 180 | { url = "https://files.pythonhosted.org/packages/dd/49/de869130028fb8d90e25da3b7d8fb13e40f5afa4c4af1781583eb1ff3839/pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", size = 12500886 }, 181 | { url = "https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", size = 11340320 }, 182 | { url = "https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", size = 15204346 }, 183 | { url = "https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad", size = 12733396 }, 184 | { url = "https://files.pythonhosted.org/packages/35/9d/208febf8c4eb5c1d9ea3314d52d8bd415fd0ef0dd66bb24cc5bdbc8fa71a/pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", size = 15858913 }, 185 | { url = "https://files.pythonhosted.org/packages/99/d1/2d9bd05def7a9e08a92ec929b5a4c8d5556ec76fae22b0fa486cbf33ea63/pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", size = 13417786 }, 186 | { url = "https://files.pythonhosted.org/packages/22/a5/a0b255295406ed54269814bc93723cfd1a0da63fb9aaf99e1364f07923e5/pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", size = 11498828 }, 187 | ] 188 | 189 | [[package]] 190 | name = "pandas-stubs" 191 | version = "2.2.2.240807" 192 | source = { registry = "https://pypi.org/simple" } 193 | dependencies = [ 194 | { name = "numpy" }, 195 | { name = "types-pytz" }, 196 | ] 197 | sdist = { url = "https://files.pythonhosted.org/packages/1f/df/0da95bc75c76f1e012e0bc0b76da31faaf4254e94b9870f25e6311145e98/pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93", size = 103095 } 198 | wheels = [ 199 | { url = "https://files.pythonhosted.org/packages/0a/f9/22c91632ea1b4c6165952f677bf9ad95f9ac36ffd7ef3e6450144e6d8b1a/pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e", size = 157069 }, 200 | ] 201 | 202 | [[package]] 203 | name = "pydantic" 204 | version = "1.10.18" 205 | source = { registry = "https://pypi.org/simple" } 206 | dependencies = [ 207 | { name = "typing-extensions" }, 208 | ] 209 | sdist = { url = "https://files.pythonhosted.org/packages/20/e6/89d6ba0c0a981fd7e3129d105502c4cf73fad1611b294c87b103f75b5837/pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", size = 354731 } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/42/c1/d5f1a844e2bc08ccb0330d2d656595e177aba494d69a3fd42e3a35f11526/pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", size = 2582713 }, 212 | { url = "https://files.pythonhosted.org/packages/1b/f4/0cf037f5c03c4d8aa18439d0146e11c2cfcca936388993a6868c803f3db1/pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", size = 2268850 }, 213 | { url = "https://files.pythonhosted.org/packages/d1/34/1a9c2745e28ca5891adbff2730c1f81f3e191aeb1c2108c8c7757dfa1989/pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", size = 3088192 }, 214 | { url = "https://files.pythonhosted.org/packages/32/d7/62134d1662af1af476b1103aea8b3ab928ff737624f8e23beeef1fbffde1/pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", size = 3119361 }, 215 | { url = "https://files.pythonhosted.org/packages/cf/e2/0e3b2250f470faa4ff150ff5e6729e1cec62e35abfc8f5150f4e794e4585/pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", size = 3163945 }, 216 | { url = "https://files.pythonhosted.org/packages/a2/de/ed086c435121c4bf915720528637e4fe7edc9b00057337512b122f1bc2db/pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", size = 3114871 }, 217 | { url = "https://files.pythonhosted.org/packages/8d/e0/c50b1492846e946eed84b24d673ca7648f68a219c61c1a100f5b892089b4/pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", size = 2118200 }, 218 | { url = "https://files.pythonhosted.org/packages/be/27/baa1ec62b24590bcf86e2c682cc47a7f57b5fd301cd2354c141a27adf2c4/pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", size = 2409940 }, 219 | { url = "https://files.pythonhosted.org/packages/b1/db/5189378620fef502cd1e7002dfe0d9f129e09eb1e4a51476ee5f4fbaeeab/pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", size = 2165312 }, 220 | { url = "https://files.pythonhosted.org/packages/9b/8b/85ab3deec6cc18c1cabd5d8f329f26267d5bd2c4b6773a75c47f4e5cfd64/pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", size = 2798664 }, 221 | { url = "https://files.pythonhosted.org/packages/4c/f9/1e1badfe27769d1f18d23c268776f67cff20e5411c2931da6bed0805fcc7/pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", size = 2829080 }, 222 | { url = "https://files.pythonhosted.org/packages/35/4d/5a4e28a95e9fac3c3964836b10074f6b7cd789d6491876333c3ffbf8c14d/pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", size = 2862046 }, 223 | { url = "https://files.pythonhosted.org/packages/da/66/ab281b4a0754fcfce903d0fd6507341e7278b3a58fb6a0d9ad2240f09348/pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", size = 2822412 }, 224 | { url = "https://files.pythonhosted.org/packages/0b/6f/273755a76305ac86f7244623a60203cc0a6ab030d0daced3d4966e2023a2/pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", size = 1944247 }, 225 | { url = "https://files.pythonhosted.org/packages/67/a4/0048b8c96b97147de57f102034dd20a35178ff70cb28707e1fb17570c1bc/pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", size = 165698 }, 226 | ] 227 | 228 | [[package]] 229 | name = "python-dateutil" 230 | version = "2.9.0.post0" 231 | source = { registry = "https://pypi.org/simple" } 232 | dependencies = [ 233 | { name = "six" }, 234 | ] 235 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 236 | wheels = [ 237 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 238 | ] 239 | 240 | [[package]] 241 | name = "python-dotenv" 242 | version = "1.0.1" 243 | source = { registry = "https://pypi.org/simple" } 244 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 245 | wheels = [ 246 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 247 | ] 248 | 249 | [[package]] 250 | name = "python-multipart" 251 | version = "0.0.9" 252 | source = { registry = "https://pypi.org/simple" } 253 | sdist = { url = "https://files.pythonhosted.org/packages/5c/0f/9c55ac6c84c0336e22a26fa84ca6c51d58d7ac3a2d78b0dfa8748826c883/python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026", size = 31516 } 254 | wheels = [ 255 | { url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215", size = 22299 }, 256 | ] 257 | 258 | [[package]] 259 | name = "pytz" 260 | version = "2024.1" 261 | source = { registry = "https://pypi.org/simple" } 262 | sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214 } 263 | wheels = [ 264 | { url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474 }, 265 | ] 266 | 267 | [[package]] 268 | name = "ruff" 269 | version = "0.6.2" 270 | source = { registry = "https://pypi.org/simple" } 271 | sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 } 272 | wheels = [ 273 | { url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 }, 274 | { url = "https://files.pythonhosted.org/packages/ae/c3/8aac62ac4638c14a740ee76a755a925f2d0d04580ab790a9887accb729f6/ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", size = 9459354 }, 275 | { url = "https://files.pythonhosted.org/packages/2f/cf/77fbd8d4617b9b9c503f9bffb8552c4e3ea1a58dc36975e7a9104ffb0f85/ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", size = 9163871 }, 276 | { url = "https://files.pythonhosted.org/packages/05/1c/765192bab32b79efbb498b06f0b9dcb3629112b53b8777ae1d19b8209e09/ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", size = 10096250 }, 277 | { url = "https://files.pythonhosted.org/packages/08/d0/86f3cb0f6934c99f759c232984a5204d67a26745cad2d9edff6248adf7d2/ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", size = 9475376 }, 278 | { url = "https://files.pythonhosted.org/packages/cd/cc/4c8d0e225b559a3fae6092ec310d7150d3b02b4669e9223f783ef64d82c0/ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", size = 10295634 }, 279 | { url = "https://files.pythonhosted.org/packages/db/96/d2699cfb1bb5a01c68122af43454c76c31331e1c8a9bd97d653d7c82524b/ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", size = 11024941 }, 280 | { url = "https://files.pythonhosted.org/packages/8b/a9/6ecd66af8929e0f2a1ed308a4137f3521789f28f0eb97d32c2ca3aa7000c/ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", size = 10606894 }, 281 | { url = "https://files.pythonhosted.org/packages/e4/73/2ee4cd19f44992fedac1cc6db9e3d825966072f6dcbd4032f21cbd063170/ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", size = 11552886 }, 282 | { url = "https://files.pythonhosted.org/packages/60/4c/c0f1cd35ce4a93c54a6bb1ee6934a3a205fa02198dd076678193853ceea1/ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", size = 10264945 }, 283 | { url = "https://files.pythonhosted.org/packages/c4/89/e45c9359b9cdd4245512ea2b9f2bb128a997feaa5f726fc9e8c7a66afadf/ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", size = 10100007 }, 284 | { url = "https://files.pythonhosted.org/packages/06/74/0bd4e0a7ed5f6908df87892f9bf60a2356c0fd74102d8097298bd9b4f346/ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", size = 9559267 }, 285 | { url = "https://files.pythonhosted.org/packages/54/03/3dc6dc9419f276f05805bf888c279e3e0b631284abd548d9e87cebb93aec/ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", size = 9905304 }, 286 | { url = "https://files.pythonhosted.org/packages/5c/5b/d6a72a6a6bbf097c09de468326ef5fa1c9e7aa5e6e45979bc0d984b0dbe7/ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", size = 10341480 }, 287 | { url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 }, 288 | { url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 }, 289 | { url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 }, 290 | ] 291 | 292 | [[package]] 293 | name = "six" 294 | version = "1.16.0" 295 | source = { registry = "https://pypi.org/simple" } 296 | sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, 299 | ] 300 | 301 | [[package]] 302 | name = "sniffio" 303 | version = "1.3.1" 304 | source = { registry = "https://pypi.org/simple" } 305 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 306 | wheels = [ 307 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 308 | ] 309 | 310 | [[package]] 311 | name = "starlette" 312 | version = "0.38.2" 313 | source = { registry = "https://pypi.org/simple" } 314 | dependencies = [ 315 | { name = "anyio" }, 316 | ] 317 | sdist = { url = "https://files.pythonhosted.org/packages/43/e2/d49a94ecb665b3a1c34b40c78165a737abc384fcabc843ccb14a3bd3dc37/starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75", size = 2844770 } 318 | wheels = [ 319 | { url = "https://files.pythonhosted.org/packages/c1/60/d976da9998e4f4a99e297cda09d61ce305919ea94cbeeb476dba4fece098/starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff", size = 72020 }, 320 | ] 321 | 322 | [[package]] 323 | name = "surrealdb-beta" 324 | version = "0.0.2" 325 | source = { git = "https://github.com/surrealdb/surrealdb.py#ffefd8a2354ff597f6f18115d18fa4eab45d6540" } 326 | 327 | [[package]] 328 | name = "surrealdb-openai" 329 | version = "0" 330 | source = { editable = "." } 331 | dependencies = [ 332 | { name = "fastapi" }, 333 | { name = "jinja2" }, 334 | { name = "pandas" }, 335 | { name = "pandas-stubs" }, 336 | { name = "python-dotenv" }, 337 | { name = "python-multipart" }, 338 | { name = "surrealdb-beta" }, 339 | { name = "tqdm" }, 340 | { name = "uvicorn" }, 341 | { name = "wget" }, 342 | ] 343 | 344 | [package.dev-dependencies] 345 | dev = [ 346 | { name = "ruff" }, 347 | ] 348 | 349 | [package.metadata] 350 | requires-dist = [ 351 | { name = "fastapi" }, 352 | { name = "jinja2" }, 353 | { name = "pandas" }, 354 | { name = "pandas-stubs" }, 355 | { name = "python-dotenv" }, 356 | { name = "python-multipart" }, 357 | { name = "surrealdb-beta", git = "https://github.com/surrealdb/surrealdb.py" }, 358 | { name = "tqdm" }, 359 | { name = "uvicorn" }, 360 | { name = "wget" }, 361 | ] 362 | 363 | [package.metadata.requires-dev] 364 | dev = [{ name = "ruff", specifier = ">=0.6.2" }] 365 | 366 | [[package]] 367 | name = "tqdm" 368 | version = "4.66.5" 369 | source = { registry = "https://pypi.org/simple" } 370 | dependencies = [ 371 | { name = "colorama", marker = "platform_system == 'Windows'" }, 372 | ] 373 | sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } 374 | wheels = [ 375 | { url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 }, 376 | ] 377 | 378 | [[package]] 379 | name = "types-pytz" 380 | version = "2024.1.0.20240417" 381 | source = { registry = "https://pypi.org/simple" } 382 | sdist = { url = "https://files.pythonhosted.org/packages/9b/b0/079f6f340c0051fbe03ac3a6d9fce323c9797b85380d455e1566eaf2716b/types-pytz-2024.1.0.20240417.tar.gz", hash = "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", size = 5459 } 383 | wheels = [ 384 | { url = "https://files.pythonhosted.org/packages/e8/8d/f5dc5239d59bb4a7b58e2b6d0dc6f2c2ba797b110f83cdda8479508c63dd/types_pytz-2024.1.0.20240417-py3-none-any.whl", hash = "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659", size = 5246 }, 385 | ] 386 | 387 | [[package]] 388 | name = "typing-extensions" 389 | version = "4.12.2" 390 | source = { registry = "https://pypi.org/simple" } 391 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 392 | wheels = [ 393 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 394 | ] 395 | 396 | [[package]] 397 | name = "tzdata" 398 | version = "2024.1" 399 | source = { registry = "https://pypi.org/simple" } 400 | sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } 401 | wheels = [ 402 | { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, 403 | ] 404 | 405 | [[package]] 406 | name = "uvicorn" 407 | version = "0.30.6" 408 | source = { registry = "https://pypi.org/simple" } 409 | dependencies = [ 410 | { name = "click" }, 411 | { name = "h11" }, 412 | ] 413 | sdist = { url = "https://files.pythonhosted.org/packages/5a/01/5e637e7aa9dd031be5376b9fb749ec20b86f5a5b6a49b87fabd374d5fa9f/uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", size = 42825 } 414 | wheels = [ 415 | { url = "https://files.pythonhosted.org/packages/f5/8e/cdc7d6263db313030e4c257dd5ba3909ebc4e4fb53ad62d5f09b1a2f5458/uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5", size = 62835 }, 416 | ] 417 | 418 | [[package]] 419 | name = "wget" 420 | version = "3.2" 421 | source = { registry = "https://pypi.org/simple" } 422 | sdist = { url = "https://files.pythonhosted.org/packages/47/6a/62e288da7bcda82b935ff0c6cfe542970f04e29c756b0e147251b2fb251f/wget-3.2.zip", hash = "sha256:35e630eca2aa50ce998b9b1a127bb26b30dfee573702782aa982f875e3f16061", size = 10857 } 423 | --------------------------------------------------------------------------------