├── 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 |
7 |
{{ content | safe }}
8 |
9 |
--------------------------------------------------------------------------------
/templates/send_user_message.html:
--------------------------------------------------------------------------------
1 |
3 |
7 |
{{ content | safe }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/templates/load_chat.html:
--------------------------------------------------------------------------------
1 |
2 | {% for message in messages %}
3 |
4 |
8 |
{{ message.content | safe }}
9 |
10 | {% endfor %}
11 |
12 |
13 |
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 |
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 |
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 |
--------------------------------------------------------------------------------