commit 56531cbd1822116fb872b22f34e0e1a71e3cf2b9 Author: Sam Partee Date: Mon Apr 22 14:13:09 2024 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8387a60a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +.idea/ +.env +venv/ +.mypy_cache/ +backend/app/log/ +backend/app/alembic/versions/ +backend/app/static/media/ +.ruff_cache/ +.pytest_cache/ diff --git a/examples/data/make_sqlite_db.py b/examples/data/make_sqlite_db.py new file mode 100644 index 00000000..5dc6b265 --- /dev/null +++ b/examples/data/make_sqlite_db.py @@ -0,0 +1,38 @@ +import csv +import sqlite3 + +# Path to the CSV file +csv_file_path = './synthetic_people_data.csv' + +# Connect to a SQLite database (will be created if it doesn't exist) +conn = sqlite3.connect('people.sqlite') +cur = conn.cursor() + +# Create a table +cur.execute(''' +CREATE TABLE IF NOT EXISTS people ( + id INTEGER PRIMARY KEY, + Name TEXT, + Age INTEGER, + Location TEXT, + Occupation TEXT, + Email TEXT +) +''') + +# Read data from the CSV file +with open(csv_file_path, 'r') as csvfile: + csvreader = csv.reader(csvfile) + next(csvreader) # Skip the header row + for row in csvreader: + # Insert each row into the database + cur.execute(''' + INSERT INTO people (Name, Age, Location, Occupation, Email) + VALUES (?, ?, ?, ?, ?) + ''', row) + +# Commit changes and close the connection +conn.commit() +conn.close() + +print('Data imported into SQLite database successfully.') diff --git a/examples/data/people.sqlite b/examples/data/people.sqlite new file mode 100644 index 00000000..cd9278a2 Binary files /dev/null and b/examples/data/people.sqlite differ diff --git a/examples/data/synthetic_people_data.csv b/examples/data/synthetic_people_data.csv new file mode 100644 index 00000000..23ea515f --- /dev/null +++ b/examples/data/synthetic_people_data.csv @@ -0,0 +1,101 @@ +Name,Age,Location,Occupation,Email +Christopher Sharp,63,"Los Angeles, CA",Paediatric nurse,grantanderson@simmons-jackson.com +Sharon Fernandez,69,"San Diego, CA",Cabin crew,christysanders@white-love.net +Rachel Thornton,76,"San Jose, CA","Engineer, electrical",douglas57@hotmail.com +Richard Moore,90,"San Jose, CA",Audiological scientist,myersdaniel@williams-gutierrez.org +Kevin Fletcher,22,"San Jose, CA",Barrister's clerk,hjohnson@hotmail.com +John Beasley,57,"San Jose, CA",Product/process development scientist,thompsonpamela@gmail.com +Luis Anderson,88,"San Antonio, TX","Engineer, biomedical",phale@gmail.com +Barbara Hodges,84,"Dallas, TX",Amenity horticulturist,maciaskatherine@knight.com +Carla Weber,95,"Chicago, IL",Community pharmacist,amycampbell@stanley.info +Sarah Little,75,"Chicago, IL",Horticultural consultant,jenniferbrown@carroll.org +Jessica Greer,93,"San Antonio, TX","Engineer, broadcasting (operations)",xnelson@day-williams.com +Brent Arnold,27,"San Antonio, TX",Lobbyist,pjacobs@hotmail.com +Natalie Trujillo,59,"Houston, TX",Dramatherapist,john40@ashley.com +Megan Gonzalez,87,"Houston, TX","Education officer, community",jacqueline01@hotmail.com +Steven Davila,60,"Dallas, TX",Psychiatrist,denisedominguez@hotmail.com +Joseph Parker,44,"Los Angeles, CA","Scientist, product/process development",mendozamatthew@yahoo.com +Jennifer Travis,56,"Dallas, TX",Automotive engineer,hbaker@gmail.com +Mr. Kenneth Phillips DVM,81,"Dallas, TX",Community arts worker,kmorton@gmail.com +Dean Moore,98,"Dallas, TX","Engineer, site",bushdenise@gmail.com +Dr. Margaret Ford,94,"Philadelphia, PA",Stage manager,mclarke@hotmail.com +Jennifer Morrow,65,"San Antonio, TX",Best boy,marshallrobert@hotmail.com +Robert Rivera,45,"San Antonio, TX",Call centre manager,isaac50@russell-wagner.info +Tammy Powell,72,"San Antonio, TX","Editor, commissioning",wsmith@hotmail.com +John Hughes,81,"Phoenix, AZ",Holiday representative,cameron72@wright.info +Mary Smith,50,"Phoenix, AZ",Location manager,bergdennis@yahoo.com +Laurie Burke,79,"San Antonio, TX",Event organiser,steven32@yahoo.com +Kevin Smith,49,"Dallas, TX",Accommodation manager,boydhannah@yahoo.com +John Foster,80,"Houston, TX",Field seismologist,garrettmartin@dixon.com +Ashlee Charles,63,"San Jose, CA","Editor, commissioning",rholmes@vega.info +Cynthia Potts,34,"New York, NY","Lecturer, higher education",ulang@vasquez.org +Jesus Brady,61,"Dallas, TX",Forensic scientist,gonzalezbrenda@gmail.com +Jamie Smith,59,"Philadelphia, PA",Retail buyer,taylorthomas@soto.com +Abigail Hicks,34,"Chicago, IL","Therapist, speech and language",lori49@gmail.com +Thomas Rodriguez,63,"San Jose, CA",Retail banker,erica79@glover-forbes.com +Eric Wilkinson,75,"San Antonio, TX",Art gallery manager,pnixon@callahan.com +Matthew Cantu,39,"Los Angeles, CA",Chief Financial Officer,reynoldsgregory@gmail.com +Chloe Chapman,35,"Philadelphia, PA",Production manager,bdavis@hotmail.com +Sophia Jones,48,"San Jose, CA",Camera operator,gsantos@gmail.com +David Young,56,"Houston, TX","Psychologist, clinical",leslie55@hotmail.com +Stacy Morris,72,"Los Angeles, CA",Logistics and distribution manager,qhansen@yahoo.com +Julie Lin,56,"Los Angeles, CA",Social worker,gzimmerman@gmail.com +Melinda White,92,"Philadelphia, PA",Probation officer,pamela45@yahoo.com +Elizabeth Daniel,95,"Dallas, TX",Sports coach,lauriegriffin@fletcher-garcia.com +Stephanie Fuller,86,"San Antonio, TX",Heritage manager,benjamin88@gmail.com +Peter Salinas,32,"Philadelphia, PA",Building control surveyor,charles57@martin-lane.com +Michael Hart,83,"Dallas, TX",Purchasing manager,john23@hotmail.com +Roger Oconnor,75,"Chicago, IL",Arts administrator,heather19@yahoo.com +John Stanley,25,"San Jose, CA",Training and development officer,kyle24@gmail.com +William Hatfield,39,"Los Angeles, CA",Adult guidance worker,ajohnson@hoover.net +Brent Stout,72,"San Diego, CA",Fish farm manager,marcus27@richardson-jones.com +Mitchell Jackson,66,"New York, NY",Quantity surveyor,nclark@hotmail.com +Jake Davila,28,"San Diego, CA",Quality manager,victorcastro@yahoo.com +Doris Hodge,72,"San Antonio, TX",Computer games developer,watersscott@hotmail.com +Erica Harris,88,"San Diego, CA",Secondary school teacher,achurch@burton-mejia.com +Joseph Hansen,59,"Dallas, TX",Medical secretary,olsenricardo@yahoo.com +Barbara Stout,72,"San Jose, CA",Clinical biochemist,victoriabrewer@allen.com +Sandra Gonzalez,73,"Chicago, IL","Engineer, maintenance",james02@snyder.com +Matthew Jones,43,"Philadelphia, PA",Electronics engineer,craigperez@moore.biz +Terry Nichols,93,"Philadelphia, PA",Outdoor activities/education manager,rogersnicole@chang-stanley.com +James Berry,94,"San Jose, CA","Buyer, industrial",serranochristopher@harrison-williams.org +Angela Sandoval,71,"Phoenix, AZ","Optician, dispensing",hurleyjennifer@williams.org +John Hawkins,38,"San Antonio, TX","Programmer, systems",johnsondonna@miller.com +Thomas Cooper,45,"San Antonio, TX",Advertising art director,gonzalezjeffrey@nelson-reynolds.com +Olivia Owens,86,"San Diego, CA","Buyer, retail",robertsontyler@edwards-hansen.com +Kelli Rodriguez,34,"Philadelphia, PA","Therapist, horticultural",tara21@gmail.com +Jeanette Briggs,61,"Philadelphia, PA",Tourism officer,alexander42@jones.com +Kimberly Perry,71,"Dallas, TX",Firefighter,conwaymichelle@duran.org +Jorge Brandt,74,"Phoenix, AZ","Programmer, systems",veronicafigueroa@weber-johnson.com +Brent Hayes,71,"New York, NY","Conservator, furniture",sgallagher@cook-marshall.net +Julie Blake,99,"San Diego, CA",Colour technologist,chriswilson@hawkins.com +David Jackson MD,33,"New York, NY",Arts administrator,melissa67@davila-johnson.com +Anna Nunez,28,"Dallas, TX",Plant breeder/geneticist,lhernandez@rodriguez.org +Caleb Adams,38,"San Diego, CA",Podiatrist,ugonzales@mitchell-lopez.info +Morgan Haas,88,"San Jose, CA",Toxicologist,michael10@yahoo.com +Emily Baker,22,"Phoenix, AZ",Equities trader,bherring@hotmail.com +Madison Edwards,33,"Houston, TX",Field seismologist,shawn27@webb.com +Edwin Conley,63,"San Jose, CA",Communications engineer,christinatapia@mendez-hughes.com +Meredith Randall,57,"Los Angeles, CA",Exercise physiologist,andrewneal@yahoo.com +George Ramos,95,"New York, NY",Energy engineer,charles02@hotmail.com +Harry Watts,77,"San Diego, CA","Copywriter, advertising",david08@warner.com +Lisa Lee,77,"Philadelphia, PA",Tourism officer,alishapratt@gmail.com +John Watson,19,"Houston, TX",Chief Executive Officer,robynhernandez@gmail.com +Joseph Sanchez,20,"Chicago, IL",Phytotherapist,allisonlauren@mcintyre.com +Seth Young,18,"Chicago, IL",Economist,elizabethbrown@hall-heath.com +Luis Gentry,55,"Houston, TX",Technical author,cpowers@williams.org +Dawn Jones,39,"Dallas, TX","Designer, television/film set",amber85@jackson-lee.com +David Davila,81,"Los Angeles, CA",Adult nurse,melissawilson@cantrell-reyes.org +Amy Donaldson,49,"San Diego, CA",Computer games developer,abigailgomez@gmail.com +Christine Burke,84,"Philadelphia, PA",Energy manager,obrown@smith-mckinney.org +Krista Gordon,83,"Chicago, IL",Computer games developer,kelleynicole@grant.com +Vanessa Gibson,67,"Chicago, IL",Armed forces operational officer,nicholasreid@wright-jones.com +Wendy Palmer,47,"Houston, TX","Engineer, maintenance (IT)",ymeyer@hotmail.com +Alicia Bass,86,"Houston, TX",Applications developer,joneslinda@adams-conley.com +Jason Lyons,33,"Philadelphia, PA",Newspaper journalist,kimberlysharp@ballard.info +Travis Cohen,98,"San Antonio, TX",Music tutor,drakejohn@castro.info +Miss Regina Bullock,72,"Houston, TX",Fisheries officer,amymercer@hotmail.com +Kevin Johnson,80,"San Antonio, TX",Futures trader,sampsonkimberly@hotmail.com +Mark Bailey,66,"San Jose, CA","Programmer, applications",nanderson@west-barajas.com +Dustin Clark,91,"Dallas, TX","Teacher, early years/pre",barrettjohn@miller.com +Autumn Reed,29,"Phoenix, AZ",Ophthalmologist,hannah08@hotmail.com diff --git a/examples/data/test_prompt.py b/examples/data/test_prompt.py new file mode 100644 index 00000000..e973e88d --- /dev/null +++ b/examples/data/test_prompt.py @@ -0,0 +1,24 @@ +from sqlalchemy import create_engine, MetaData + +# Replace 'your_database.db' with your actual SQLite database file +database_path = 'sqlite:///people.sqlite' +engine = create_engine(database_path) +metadata = MetaData() + +# Reflect the tables in the database +metadata.reflect(bind=engine) + +# Iterate over all tables and print their descriptions +for table_name in metadata.tables: + print(f"Table: {table_name}") + table = metadata.tables[table_name] + + # Iterate over columns in the table and print details + for column in table.c: + print(f"Column: {column.name}") + print(f"Type: {column.type}") + print(f"Nullable: {column.nullable}") + print(f"Primary Key: {column.primary_key}") + print(f"---------------------") + + print(f"{'='*20}\n") \ No newline at end of file diff --git a/examples/gmail/pack.lock.toml b/examples/gmail/pack.lock.toml new file mode 100644 index 00000000..cfd84f29 --- /dev/null +++ b/examples/gmail/pack.lock.toml @@ -0,0 +1,12 @@ +[pack] +name = "example" +description = "A simple actor that sends an email using the Gmail API." +version = "0.1.0" +author = "Sam Partee" +email = "sam@partee.io" + +[depends] + +[tools] +SendEmail = "gmailer.send_email@0.1.0" +ReadEmail = "gmailer.read_email@0.1.0" diff --git a/examples/gmail/pack.toml b/examples/gmail/pack.toml new file mode 100644 index 00000000..f189689b --- /dev/null +++ b/examples/gmail/pack.toml @@ -0,0 +1,11 @@ + + +[pack] +name = "example" +description = "A simple actor that sends an email using the Gmail API." +version = "0.1.0" +author = "Sam Partee" +email = "sam@partee.io" + +[modules] +gmailer = "0.1.0" diff --git a/examples/gmail/tools/gmailer.py b/examples/gmail/tools/gmailer.py new file mode 100644 index 00000000..599cfd29 --- /dev/null +++ b/examples/gmail/tools/gmailer.py @@ -0,0 +1,92 @@ + +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import imaplib +import email +from email.header import decode_header +from pydantic import BaseModel +import pandas as pd + + +from toolserve.sdk import Param, Secret, tool + + +@tool +def send_email( + sender_email: Param(str, "Email address of the sender"), + sender_password: Secret(str, "gmail_password"), + recipient_email: Param(str, "Email address of the recipient"), + subject: Param(str, "Subject of the email"), + body: Param(str, "Body of the email"), + server: Secret(str, "gmail_stmp_server"), + port: Secret(str, "gmail_smtp_port") + ): + """Send an email via gmail SMTP server""" + + message = MIMEMultipart() + message['From'] = sender_email + message['To'] = recipient_email + message['Subject'] = subject + message.attach(MIMEText(body, 'plain')) + + server = smtplib.SMTP(server, port) + server.starttls() + server.login(sender_email, sender_password) + print("Logged in to SMTP server") + + server.send_message(message) + server.quit() + + print(f"Email sent to {recipient_email}") + + + +@tool +def read_email( + email: Param(str, "Email address of the recipient"), + password: Secret(str, "gmail_password"), + server: Secret(str, "gmail_stmp_server"), + port: Secret(int, "gmail_smtp_port") + ) -> Param(str, "JSON dataframe of List of emails"): + """Read emails from a Gmail account""" + + + # Connect to the Gmail IMAP server + mail = imaplib.IMAP4_SSL(server) + mail.login(email, password) + mail.select("inbox") # connect to inbox. + + result, data = mail.search(None, "ALL") + email_ids = data[0].split() + + emails = [] + + for email_id in email_ids: + result, data = mail.fetch(email_id, "(RFC822)") + raw_email = data[0][1] + msg = email.message_from_bytes(raw_email) + + email_details = { + "from": msg["From"], + "to": msg["To"], + "subject": decode_header(msg["Subject"])[0][0], + "date": msg["Date"] + } + + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == "text/plain": + email_details["body"] = part.get_payload(decode=True).decode() + else: + email_details["body"] = msg.get_payload(decode=True).decode() + + emails.append(email_details) + + mail.close() + mail.logout() + + return pd.DataFrame(emails).to_json() + + + diff --git a/toolserve/README.md b/toolserve/README.md new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/poetry.lock b/toolserve/poetry.lock new file mode 100644 index 00000000..cfdbf97e --- /dev/null +++ b/toolserve/poetry.lock @@ -0,0 +1,663 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.1.1" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.110.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"}, + {file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "msgpack" +version = "1.0.8" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, +] + +[[package]] +name = "msgspec" +version = "0.18.6" +description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgspec-0.18.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77f30b0234eceeff0f651119b9821ce80949b4d667ad38f3bfed0d0ebf9d6d8f"}, + {file = "msgspec-0.18.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a76b60e501b3932782a9da039bd1cd552b7d8dec54ce38332b87136c64852dd"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06acbd6edf175bee0e36295d6b0302c6de3aaf61246b46f9549ca0041a9d7177"}, + {file = "msgspec-0.18.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a4df891676d9c28a67c2cc39947c33de516335680d1316a89e8f7218660410"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6896f4cd5b4b7d688018805520769a8446df911eb93b421c6c68155cdf9dd5a"}, + {file = "msgspec-0.18.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3ac4dd63fd5309dd42a8c8c36c1563531069152be7819518be0a9d03be9788e4"}, + {file = "msgspec-0.18.6-cp310-cp310-win_amd64.whl", hash = "sha256:fda4c357145cf0b760000c4ad597e19b53adf01382b711f281720a10a0fe72b7"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e77e56ffe2701e83a96e35770c6adb655ffc074d530018d1b584a8e635b4f36f"}, + {file = "msgspec-0.18.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5351afb216b743df4b6b147691523697ff3a2fc5f3d54f771e91219f5c23aaa"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3232fabacef86fe8323cecbe99abbc5c02f7698e3f5f2e248e3480b66a3596b"}, + {file = "msgspec-0.18.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b524df6ea9998bbc99ea6ee4d0276a101bcc1aa8d14887bb823914d9f60d07"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37f67c1d81272131895bb20d388dd8d341390acd0e192a55ab02d4d6468b434c"}, + {file = "msgspec-0.18.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0feb7a03d971c1c0353de1a8fe30bb6579c2dc5ccf29b5f7c7ab01172010492"}, + {file = "msgspec-0.18.6-cp311-cp311-win_amd64.whl", hash = "sha256:41cf758d3f40428c235c0f27bc6f322d43063bc32da7b9643e3f805c21ed57b4"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d86f5071fe33e19500920333c11e2267a31942d18fed4d9de5bc2fbab267d28c"}, + {file = "msgspec-0.18.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce13981bfa06f5eb126a3a5a38b1976bddb49a36e4f46d8e6edecf33ccf11df1"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97dec6932ad5e3ee1e3c14718638ba333befc45e0661caa57033cd4cc489466"}, + {file = "msgspec-0.18.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad237100393f637b297926cae1868b0d500f764ccd2f0623a380e2bcfb2809ca"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db1d8626748fa5d29bbd15da58b2d73af25b10aa98abf85aab8028119188ed57"}, + {file = "msgspec-0.18.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d70cb3d00d9f4de14d0b31d38dfe60c88ae16f3182988246a9861259c6722af6"}, + {file = "msgspec-0.18.6-cp312-cp312-win_amd64.whl", hash = "sha256:1003c20bfe9c6114cc16ea5db9c5466e49fae3d7f5e2e59cb70693190ad34da0"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7d9faed6dfff654a9ca7d9b0068456517f63dbc3aa704a527f493b9200b210a"}, + {file = "msgspec-0.18.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9da21f804c1a1471f26d32b5d9bc0480450ea77fbb8d9db431463ab64aaac2cf"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46eb2f6b22b0e61c137e65795b97dc515860bf6ec761d8fb65fdb62aa094ba61"}, + {file = "msgspec-0.18.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8355b55c80ac3e04885d72db515817d9fbb0def3bab936bba104e99ad22cf46"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9080eb12b8f59e177bd1eb5c21e24dd2ba2fa88a1dbc9a98e05ad7779b54c681"}, + {file = "msgspec-0.18.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc001cf39becf8d2dcd3f413a4797c55009b3a3cdbf78a8bf5a7ca8fdb76032c"}, + {file = "msgspec-0.18.6-cp38-cp38-win_amd64.whl", hash = "sha256:fac5834e14ac4da1fca373753e0c4ec9c8069d1fe5f534fa5208453b6065d5be"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:974d3520fcc6b824a6dedbdf2b411df31a73e6e7414301abac62e6b8d03791b4"}, + {file = "msgspec-0.18.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd62e5818731a66aaa8e9b0a1e5543dc979a46278da01e85c3c9a1a4f047ef7e"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7481355a1adcf1f08dedd9311193c674ffb8bf7b79314b4314752b89a2cf7f1c"}, + {file = "msgspec-0.18.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6aa85198f8f154cf35d6f979998f6dadd3dc46a8a8c714632f53f5d65b315c07"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e24539b25c85c8f0597274f11061c102ad6b0c56af053373ba4629772b407be"}, + {file = "msgspec-0.18.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c61ee4d3be03ea9cd089f7c8e36158786cd06e51fbb62529276452bbf2d52ece"}, + {file = "msgspec-0.18.6-cp39-cp39-win_amd64.whl", hash = "sha256:b5c390b0b0b7da879520d4ae26044d74aeee5144f83087eb7842ba59c02bc090"}, + {file = "msgspec-0.18.6.tar.gz", hash = "sha256:a59fc3b4fcdb972d09138cb516dbde600c99d07c38fd9372a6ef500d2d031b4e"}, +] + +[package.extras] +dev = ["attrs", "coverage", "furo", "gcovr", "ipython", "msgpack", "mypy", "pre-commit", "pyright", "pytest", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "tomli", "tomli-w"] +doc = ["furo", "ipython", "sphinx", "sphinx-copybutton", "sphinx-design"] +test = ["attrs", "msgpack", "mypy", "pyright", "pytest", "pyyaml", "tomli", "tomli-w"] +toml = ["tomli", "tomli-w"] +yaml = ["pyyaml"] + +[[package]] +name = "pydantic" +version = "2.7.0" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.18.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "redis" +version = "5.0.3" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, + {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "stdlib-list" +version = "0.10.0" +description = "A list of Python Standard Libraries (2.7 through 3.12)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "stdlib_list-0.10.0-py3-none-any.whl", hash = "sha256:b3a911bc441d03e0332dd1a9e7d0870ba3bb0a542a74d7524f54fb431256e214"}, + {file = "stdlib_list-0.10.0.tar.gz", hash = "sha256:6519c50d645513ed287657bfe856d527f277331540691ddeaf77b25459964a14"}, +] + +[package.extras] +dev = ["build", "stdlib-list[doc,lint,test]"] +doc = ["furo", "sphinx"] +lint = ["black", "mypy", "ruff"] +support = ["sphobjinv"] +test = ["coverage[toml]", "pytest", "pytest-cov"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + +[[package]] +name = "typer" +version = "0.9.4" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.6" +files = [ + {file = "typer-0.9.4-py3-none-any.whl", hash = "sha256:aa6c4a4e2329d868b80ecbaf16f807f2b54e192209d7ac9dd42691d63f7a54eb"}, + {file = "typer-0.9.4.tar.gz", hash = "sha256:f714c2d90afae3a7929fcd72a3abb08df305e1ff61719381384211c4070af57f"}, +] + +[package.dependencies] +click = ">=7.1.1,<9.0.0" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "uvicorn" +version = "0.28.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.28.1-py3-none-any.whl", hash = "sha256:5162f6d652f545be91b1feeaee8180774af143965ca9dc8a47ff1dc6bafa4ad5"}, + {file = "uvicorn-0.28.1.tar.gz", hash = "sha256:08103e79d546b6cf20f67c7e5e434d2cf500a6e29b28773e407250c54fc4fa3c"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "f034a070ec06e119c2f182dcfdcfeeb0c7c9bc4a5f9116e5c12f7f650f0678f0" diff --git a/toolserve/pyproject.toml b/toolserve/pyproject.toml new file mode 100644 index 00000000..a0afea25 --- /dev/null +++ b/toolserve/pyproject.toml @@ -0,0 +1,30 @@ +[tool.poetry] +name = "toolserve" +version = "0.1.0" +description = "" +authors = ["Sam Partee "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +pydantic = {extras = ["email"], version = "^2.7.0"} +fastapi = "^0.110.0" +redis = "^5.0.3" +uvicorn = "^0.28.0" +loguru = "^0.7.2" +pydantic-settings = "^2.2.1" +msgspec = "^0.18.6" +msgpack = "^1.0.8" +typer = "^0.9.0" +rich = "^13.7.1" +toml = "^0.10.2" +tomlkit = "^0.12.4" +stdlib-list = "^0.10.0" + + +[tool.poetry.scripts] +tool = "toolserve.cli.main:cli" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/toolserve/tests/__init__.py b/toolserve/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/__init__.py b/toolserve/toolserve/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/apm/__init__.py b/toolserve/toolserve/apm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/apm/base.py b/toolserve/toolserve/apm/base.py new file mode 100644 index 00000000..e5aca0a3 --- /dev/null +++ b/toolserve/toolserve/apm/base.py @@ -0,0 +1,49 @@ + +import os +import toml +import json +import tomlkit + +from pathlib import Path +from pydantic import BaseModel, ValidationError, EmailStr, Field +from typing import Dict, List, Optional, TypeVar, Any, Tuple, Union + + +class PackInfo(BaseModel): + name: str + description: str + version: str + author: Optional[str] + email: Optional[EmailStr] + + +class ToolPack(BaseModel): + pack: PackInfo + depends: Optional[Dict[str, str]] = None + tools: Optional[Dict[str, str]] = {} + + def write_lock_file(self, pack_dir: Union[str, os.PathLike]): + lock_file = Path(pack_dir) / 'pack.lock.toml' + pack_dict = self.dict(by_alias=True, exclude_none=True) + pack_ordered_dict = { + "pack": pack_dict.get("pack"), + "depends": pack_dict.get("depends"), + "tools": pack_dict.get("tools"), + } + + # Create a tomlkit document from the ordered dictionary + doc = tomlkit.document() + for key, value in pack_ordered_dict.items(): + doc[key] = value + + # Write the tomlkit document to file + with open(lock_file, 'w') as f: + f.write(tomlkit.dumps(doc)) + + @classmethod + def from_lock_file(cls, pack_dir: Union[str, os.PathLike]): + pack_dir = Path(pack_dir).resolve() + lock_file = pack_dir / 'pack.lock.toml' + with open(lock_file, 'r') as f: + data = toml.load(f) + return cls(**data) \ No newline at end of file diff --git a/toolserve/toolserve/apm/pack.py b/toolserve/toolserve/apm/pack.py new file mode 100644 index 00000000..dd49cc68 --- /dev/null +++ b/toolserve/toolserve/apm/pack.py @@ -0,0 +1,76 @@ +import os +import json +import toml +import shutil + +from pathlib import Path +from typing import List, Optional, Dict, Union +from pydantic import BaseModel, Field, ValidationError, EmailStr + +from toolserve.apm.base import PackInfo, ToolPack +from toolserve.apm.parse import get_tools_from_file +from toolserve.utils import snake_to_camel + +class Packer: + + def __init__(self, pack_dir: Union[str, os.PathLike]): + self.pack_dir = Path(pack_dir).resolve() + self.tools_dir = self.pack_dir / 'tools' + # Load the action pack configuration from a TOML file + try: + with open(self.pack_dir / 'pack.toml', 'r') as f: + pack_data = toml.load(f) + + self.pack = PackInfo(**pack_data['pack']) + self.modules = pack_data['modules'] + + except FileNotFoundError: + raise FileNotFoundError(f"No 'pack.toml' found in {self.tools_dir}") + except (toml.TomlDecodeError, ValidationError) as e: + raise ValueError(f"Invalid 'pack.toml' format: {e}") + + self.tools = self.load_tools() + self.depends = {} # TODO + self.packs = [] # TODO + + + def load_tools(self) -> Dict[str, str]: + tools = {} + for tool_file in self.tools_dir.rglob('*.py'): + if '__init__.py' in tool_file.name: + continue + try: + module = tool_file.stem + version = self.modules.get(module, "latest") + + found_tools = get_tools_from_file(tool_file) + for tool in found_tools: + tool_name = module + "." + tool + "@" + version + tools[snake_to_camel(tool)] = tool_name + except Exception as e: + print(f"Error loading tool from {tool_file}: {e}") + return tools + + + def _create_pack_dir(self, pack: ToolPack) -> Path: + # Make "packs" directory if it doesn't exist + packs_dir = self.pack_dir / 'packs' + os.makedirs(packs_dir, exist_ok=True) + # make the dir for the action pack and the version (making parent dirs if needed) + top_pack_dir = packs_dir / pack.pack.name / pack.pack.version + # If the pack already exists, remove it and recreate it + if top_pack_dir.exists(): + shutil.rmtree(top_pack_dir) + os.makedirs(top_pack_dir, exist_ok=True) + return top_pack_dir + + def create_pack(self): + # Create an ActionPack instance from the loaded data + pack = ToolPack( + pack=self.pack, + depends=self.depends, + tools=self.tools + ) + #pack_dir = self._create_pack_dir(pack) + # Write the action pack to a TOML file + pack.write_lock_file(self.pack_dir) diff --git a/toolserve/toolserve/apm/parse.py b/toolserve/toolserve/apm/parse.py new file mode 100644 index 00000000..7048facf --- /dev/null +++ b/toolserve/toolserve/apm/parse.py @@ -0,0 +1,83 @@ +import ast +import sys +import os +import shutil +from typing import Dict, List, Optional, Tuple, Any +import importlib.metadata +import importlib.util +import toml +from stdlib_list import stdlib_list + +def load_ast_tree(filepath: str) -> ast.AST: + """ + Load and parse the Abstract Syntax Tree (AST) from a Python file. + + :param filepath: Path to the Python file. + :return: AST of the Python file. + """ + try: + with open(filepath, "r") as file: + return ast.parse(file.read(), filename=filepath) + except FileNotFoundError: + raise FileNotFoundError(f"File {filepath} not found") + +def get_python_version() -> str: + """ + Get the current Python version. + + :return: The version of Python in use. + """ + return f"{sys.version_info.major}.{sys.version_info.minor}" + +def retrieve_imported_libraries(tree: ast.AST) -> Dict[str, Optional[str]]: + """ + Retrieve non-standard libraries imported in the AST. + + :param tree: The AST of the file. + :return: A dictionary with libraries as keys and their versions as values. + """ + libraries = {} + python_version = get_python_version() + stdlib_modules = stdlib_list(python_version) + + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + package_name = node.module.split('.')[0] if node.module else None + if package_name == 'dstar' or package_name in stdlib_modules: + continue + try: + package_version = importlib.metadata.version(package_name) + except importlib.metadata.PackageNotFoundError: + package_version = None + libraries[package_name] = package_version + return libraries + + +def get_function_name_if_decorated(node: ast.FunctionDef) -> Optional[str]: + """ + Check if a function has a decorator of either "@toolserve.tool" or "tool" and return the function's name. + + :param node: The function definition node from the AST. + :return: The name of the function if it has the specified decorators, otherwise None. + """ + decorator_ids = {'toolserve.tool', 'tool'} + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id in decorator_ids: + return node.name + return None + +def get_tools_from_file(filepath: str) -> List[str]: + """ + Get the names of all functions in a Python file that are decorated with either "@toolserve.tool" or "@tool". + + :param filepath: Path to the Python file. + :return: List of function names. + """ + tree = load_ast_tree(filepath) + tools = [] + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + tool_name = get_function_name_if_decorated(node) + if tool_name: + tools.append(tool_name) + return tools \ No newline at end of file diff --git a/toolserve/toolserve/cli/__init__.py b/toolserve/toolserve/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/cli/main.py b/toolserve/toolserve/cli/main.py new file mode 100644 index 00000000..0b6d3b45 --- /dev/null +++ b/toolserve/toolserve/cli/main.py @@ -0,0 +1,66 @@ +import os +import typer +import uvicorn + +from pathlib import Path +from rich.console import Console +from rich.markup import escape + +from toolserve.common.log import log +from toolserve.server.core.conf import settings +from toolserve.server.main import app +from toolserve.apm.pack import Packer + + +cli = typer.Typer() +console = Console() + +@cli.command(help="Starts the ToolServer with specified configurations.") +def serve( + host: str = typer.Option( + settings.UVICORN_HOST, + help="Host for the app, from settings by default.", + show_default=True + ), + port: int = typer.Option( + settings.UVICORN_PORT, + help="Port for the app, settings default.", + show_default=True + ), +): + """ + Starts the server with host, port, and reload options. Uses + Uvicorn as ASGI server. Parameters allow runtime configuration. + """ + try: + uvicorn.run( + app=app, + host=host, + port=port, + ) + except KeyboardInterrupt: + console.print("Server stopped by user.", style="bold red") + typer.Exit() + except Exception as e: + error_message = f'❌ Failed to start Toolserver: {escape(str(e))}' + console.print(error_message, style="bold red") + raise typer.Exit(code=1) + +@cli.command(help="Build a new Tool Pack") +def pack( + directory: str = typer.Option( + os.getcwd(), + "--dir", + help="tools directory path with pack.toml" + ), +): + """ + Creates a new tool pack with the given name, description, and result type. + """ + try: + pack = Packer(directory) + pack.create_pack() + except Exception as e: + error_message = f'❌ Failed to build Tool Pack: {escape(str(e))}' + console.print(error_message, style="bold red") + raise typer.Exit(code=1) \ No newline at end of file diff --git a/toolserve/toolserve/common/__init__.py b/toolserve/toolserve/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/common/log.py b/toolserve/toolserve/common/log.py new file mode 100644 index 00000000..f6912a15 --- /dev/null +++ b/toolserve/toolserve/common/log.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +import os + +from typing import TYPE_CHECKING + +from loguru import logger + +from toolserve.server.core.conf import settings + +from pathlib import Path + +server_log_path = os.path.join(settings.WORK_DIR, 'server_logs') + + +if TYPE_CHECKING: + import loguru + + +class Logger: + def __init__(self): + self.log_path = server_log_path + + def log(self) -> loguru.Logger: + if not os.path.exists(self.log_path): + os.makedirs(self.log_path, exist_ok=True) + + log_stdout_file = os.path.join(self.log_path, settings.LOG_STDOUT_FILENAME) + log_stderr_file = os.path.join(self.log_path, settings.LOG_STDERR_FILENAME) + + log_config = dict(rotation='10 MB', retention='15 days', compression='tar.gz', enqueue=True) + # stdout + logger.add( + log_stdout_file, + level='INFO', + filter=lambda record: record['level'].name == 'INFO' or record['level'].no <= 25, + **log_config, + backtrace=False, + diagnose=False, + ) + # stderr + logger.add( + log_stderr_file, + level='ERROR', + filter=lambda record: record['level'].name == 'ERROR' or record['level'].no >= 30, + **log_config, + backtrace=True, + diagnose=True, + ) + + return logger + +log = Logger().log() \ No newline at end of file diff --git a/toolserve/toolserve/common/response.py b/toolserve/toolserve/common/response.py new file mode 100644 index 00000000..04a5bbc5 --- /dev/null +++ b/toolserve/toolserve/common/response.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from toolserve.common.response_code import CustomResponse, CustomResponseCode +from toolserve.server.core.conf import settings + +_ExcludeData = set[int | str] | dict[int | str, Any] + +__all__ = ['ResponseModel', 'response_base'] + + +class ResponseModel(BaseModel): + """ + # Unified return model + E.g. :: + + @router.get('/test', response_model=ResponseModel) + def test(): + return ResponseModel(data={'test': 'test'}) + + @router.get('/test') + def test() -> ResponseModel: + return ResponseModel(data={'test': 'test'}) + + @router.get('/test') + def test() -> ResponseModel: + res = CustomResponseCode.HTTP_200 + return ResponseModel(code=res.code, msg=res.msg, data={'test': 'test'}) + """ + + # TODO: json_encoders: https://github.com/tiangolo/fastapi/discussions/10252 + model_config = ConfigDict(json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)}) + + code: int = CustomResponseCode.HTTP_200.code + msg: str = CustomResponseCode.HTTP_200.msg + data: Any | None = None + + +class ResponseBase: + """ + Unified return method + + .. tip:: + + The methods in this class will return the ResponseModel model, existing as a coding style; + + E.g. :: + + @router.get('/test') + def test() -> ResponseModel: + return await response_base.success(data={'test': 'test'}) + """ + + @staticmethod + async def __response(*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None) -> ResponseModel: + """ + General method for successful response + + :param res: Response information + :param data: Response data + :return: + """ + return ResponseModel(code=res.code, msg=res.msg, data=data) + + async def success( + self, + *, + res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_200, + data: Any | None = None, + ) -> ResponseModel: + return await self.__response(res=res, data=data) + + async def fail( + self, + *, + res: CustomResponseCode | CustomResponse = CustomResponseCode.HTTP_400, + data: Any = None, + ) -> ResponseModel: + return await self.__response(res=res, data=data) + + +response_base = ResponseBase() diff --git a/toolserve/toolserve/common/response_code.py b/toolserve/toolserve/common/response_code.py new file mode 100644 index 00000000..2d97a21a --- /dev/null +++ b/toolserve/toolserve/common/response_code.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import dataclasses + +from enum import Enum + + +class CustomCodeBase(Enum): + """自定义状态码基类""" + + @property + def code(self): + """ + 获取状态码 + """ + return self.value[0] + + @property + def msg(self): + """ + 获取状态码信息 + """ + return self.value[1] + + +class CustomResponseCode(CustomCodeBase): + """自定义响应状态码""" + HTTP_200 = (200, 'Request Successful') + HTTP_201 = (201, 'Created Successfully') + HTTP_202 = (202, 'Request Accepted, but Processing Not Yet Complete') + HTTP_204 = (204, 'Request Successful, but No Content Returned') + HTTP_400 = (400, 'Bad Request') + HTTP_401 = (401, 'Unauthorized') + HTTP_403 = (403, 'Forbidden Access') + HTTP_404 = (404, 'Requested Resource Not Found') + HTTP_410 = (410, 'Requested Resource Permanently Deleted') + HTTP_422 = (422, 'Invalid Request Parameters') + HTTP_425 = (425, 'Request Unexecutable, as Server Cannot Meet Requirements') + HTTP_429 = (429, 'Too Many Requests, Server Limiting') + HTTP_500 = (500, 'Internal Server Error') + HTTP_502 = (502, 'Gateway Error') + HTTP_503 = (503, 'Server Temporarily Unable to Process Request') + HTTP_504 = (504, 'Gateway Timeout') + +class CustomErrorCode(CustomCodeBase): + """自定义错误状态码""" + + CAPTCHA_ERROR = (40001, 'CAPTCHA Error') + +@dataclasses.dataclass +class CustomResponse: + """ + Provides open response status codes, rather than enums, which can be useful if you want to customize response information + """ + + code: int + msg: str + +class StandardResponseCode: + """Standard response status codes""" + + """ + HTTP codes + See HTTP Status Code Registry: + https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + + And RFC 2324 - https://tools.ietf.org/html/rfc2324 + """ + HTTP_100 = 100 # CONTINUE + HTTP_101 = 101 # SWITCHING_PROTOCOLS + HTTP_102 = 102 # PROCESSING + HTTP_103 = 103 # EARLY_HINTS + HTTP_200 = 200 # OK + HTTP_201 = 201 # CREATED + HTTP_202 = 202 # ACCEPTED + HTTP_203 = 203 # NON_AUTHORITATIVE_INFORMATION + HTTP_204 = 204 # NO_CONTENT + HTTP_205 = 205 # RESET_CONTENT + HTTP_206 = 206 # PARTIAL_CONTENT + HTTP_207 = 207 # MULTI_STATUS + HTTP_208 = 208 # ALREADY_REPORTED + HTTP_226 = 226 # IM_USED + HTTP_300 = 300 # MULTIPLE_CHOICES + HTTP_301 = 301 # MOVED_PERMANENTLY + HTTP_302 = 302 # FOUND + HTTP_303 = 303 # SEE_OTHER + HTTP_304 = 304 # NOT_MODIFIED + HTTP_305 = 305 # USE_PROXY + HTTP_307 = 307 # TEMPORARY_REDIRECT + HTTP_308 = 308 # PERMANENT_REDIRECT + HTTP_400 = 400 # BAD_REQUEST + HTTP_401 = 401 # UNAUTHORIZED + HTTP_402 = 402 # PAYMENT_REQUIRED + HTTP_403 = 403 # FORBIDDEN + HTTP_404 = 404 # NOT_FOUND + HTTP_405 = 405 # METHOD_NOT_ALLOWED + HTTP_406 = 406 # NOT_ACCEPTABLE + HTTP_407 = 407 # PROXY_AUTHENTICATION_REQUIRED + HTTP_408 = 408 # REQUEST_TIMEOUT + HTTP_409 = 409 # CONFLICT + HTTP_410 = 410 # GONE + HTTP_411 = 411 # LENGTH_REQUIRED + HTTP_412 = 412 # PRECONDITION_FAILED + HTTP_413 = 413 # REQUEST_ENTITY_TOO_LARGE + HTTP_414 = 414 # REQUEST_URI_TOO_LONG + HTTP_415 = 415 # UNSUPPORTED_MEDIA_TYPE + HTTP_416 = 416 # REQUESTED_RANGE_NOT_SATISFIABLE + HTTP_417 = 417 # EXPECTATION_FAILED + HTTP_418 = 418 # UNUSED + HTTP_421 = 421 # MISDIRECTED_REQUEST + HTTP_422 = 422 # UNPROCESSABLE_CONTENT + HTTP_423 = 423 # LOCKED + HTTP_424 = 424 # FAILED_DEPENDENCY + HTTP_425 = 425 # TOO_EARLY + HTTP_426 = 426 # UPGRADE_REQUIRED + HTTP_427 = 427 # UNASSIGNED + HTTP_428 = 428 # PRECONDITION_REQUIRED + HTTP_429 = 429 # TOO_MANY_REQUESTS + HTTP_430 = 430 # Unassigned + HTTP_431 = 431 # REQUEST_HEADER_FIELDS_TOO_LARGE + HTTP_451 = 451 # UNAVAILABLE_FOR_LEGAL_REASONS + HTTP_500 = 500 # INTERNAL_SERVER_ERROR + HTTP_501 = 501 # NOT_IMPLEMENTED + HTTP_502 = 502 # BAD_GATEWAY + HTTP_503 = 503 # SERVICE_UNAVAILABLE + HTTP_504 = 504 # GATEWAY_TIMEOUT + HTTP_505 = 505 # HTTP_VERSION_NOT_SUPPORTED + HTTP_506 = 506 # VARIANT_ALSO_NEGOTIATES + HTTP_507 = 507 # INSUFFICIENT_STORAGE + HTTP_508 = 508 # LOOP_DETECTED + HTTP_509 = 509 # UNASSIGNED + HTTP_510 = 510 # NOT_EXTENDED + HTTP_511 = 511 # NETWORK_AUTHENTICATION_REQUIRED + + """ + WebSocket codes + https://www.iana.org/assignments/websocket/websocket.xml#close-code-number + https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + """ + WS_1000 = 1000 # NORMAL_CLOSURE + WS_1001 = 1001 # GOING_AWAY + WS_1002 = 1002 # PROTOCOL_ERROR + WS_1003 = 1003 # UNSUPPORTED_DATA + WS_1005 = 1005 # NO_STATUS_RCVD + WS_1006 = 1006 # ABNORMAL_CLOSURE + WS_1007 = 1007 # INVALID_FRAME_PAYLOAD_DATA + WS_1008 = 1008 # POLICY_VIOLATION + WS_1009 = 1009 # MESSAGE_TOO_BIG + WS_1010 = 1010 # MANDATORY_EXT + WS_1011 = 1011 # INTERNAL_ERROR + WS_1012 = 1012 # SERVICE_RESTART + WS_1013 = 1013 # TRY_AGAIN_LATER + WS_1014 = 1014 # BAD_GATEWAY + WS_1015 = 1015 # TLS_HANDSHAKE + WS_3000 = 3000 # UNAUTHORIZED + WS_3003 = 3003 # FORBIDDEN diff --git a/toolserve/toolserve/common/serializers.py b/toolserve/toolserve/common/serializers.py new file mode 100644 index 00000000..affd6034 --- /dev/null +++ b/toolserve/toolserve/common/serializers.py @@ -0,0 +1,15 @@ +from decimal import Decimal +from typing import Any, Sequence, TypeVar + +import msgspec + +from starlette.responses import JSONResponse + + +class MsgSpecJSONResponse(JSONResponse): + """ + JSON response using the high-performance msgspec library to serialize data to JSON. + """ + + def render(self, content: Any) -> bytes: + return msgspec.json.encode(content) diff --git a/toolserve/toolserve/sdk/__init__.py b/toolserve/toolserve/sdk/__init__.py new file mode 100644 index 00000000..689c5286 --- /dev/null +++ b/toolserve/toolserve/sdk/__init__.py @@ -0,0 +1,6 @@ + +from .tool import ( + Param, + tool, + Secret +) \ No newline at end of file diff --git a/toolserve/toolserve/sdk/tool.py b/toolserve/toolserve/sdk/tool.py new file mode 100644 index 00000000..f7bc2ba1 --- /dev/null +++ b/toolserve/toolserve/sdk/tool.py @@ -0,0 +1,23 @@ +from typing import Annotated, TypeVar, _AnnotatedAlias, Type, Callable, Any +import functools + +T = TypeVar('T') +class SecretKey: + def __init__(self, key: str): + self.key = key + +class Description: + def __init__(self, description: str): + self.description = description + +def Param(type_: Type[T], description: str) -> Annotated[T, Description]: + return Annotated[type_, Description(description)] + +def Secret(type_: Type[T], key: str) -> Annotated[T, SecretKey]: + return Annotated[type_, SecretKey(key)] + +def tool(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs) -> Any: + return func(*args, **kwargs) + return wrapper diff --git a/toolserve/toolserve/server/__init__.py b/toolserve/toolserve/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/server/core/__init__.py b/toolserve/toolserve/server/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/server/core/catalog.py b/toolserve/toolserve/server/core/catalog.py new file mode 100644 index 00000000..a474a6ac --- /dev/null +++ b/toolserve/toolserve/server/core/catalog.py @@ -0,0 +1,129 @@ + + +import os +import sys +import inspect +from datetime import datetime +from typing import List, Optional, Type, Dict, Annotated, Any, Callable, Tuple +from pathlib import Path + +from fastapi import APIRouter +from pydantic import BaseModel, ValidationError, Field, create_model +from importlib import import_module + +from toolserve.server.core.conf import settings +from toolserve.common.response_code import CustomResponseCode +from toolserve.common.response import ResponseModel, response_base +from toolserve.apm.base import ToolPack +from toolserve.sdk import Param, Secret + +class ToolMeta(BaseModel): + module: str + path: str + date_added: datetime = Field(default_factory=datetime.now) + date_updated: datetime = Field(default_factory=datetime.now) + + +class ToolSchema(BaseModel): + name: str + description: str + version: str + tool: Callable + + input_model: Type[BaseModel] + output_model: Type[BaseModel] + + meta: ToolMeta + + +class ToolCatalog: + def __init__(self, tools_dir: str = settings.TOOLS_DIR): + self.tools = self.read_tools(tools_dir) + + @staticmethod + def read_tools(directory: str) -> List[ToolSchema]: + toolpack = ToolPack.from_lock_file(directory) + sys.path.append(str(Path(directory).resolve() / 'tools')) + + tools = {} + for name, tool_spec in toolpack.tools.items(): + module_name, versioned_tool = tool_spec.split('.', 1) + func_name, version = versioned_tool.split('@') + + module = import_module(module_name) + tool = getattr(module, func_name) + + tool_meta = ToolMeta( + module=module_name, + path=module.__file__ + ) + input_model, output_model = create_pydantic_models_for_ds_tool(tool) + tool_schema = ToolSchema( + name=name, + description=tool.__doc__, + version=version, + tool=tool, + input_model=input_model, + output_model=output_model, + meta=tool_meta + ) + tools[name] = tool_schema + + return tools + + def __getitem__(self, name: str) -> Optional[ToolSchema]: + #TODO error handling + for tool in self.tools: + if tool.name == name: + return tool + return None + + def get_tool(self, name: str) -> Optional[Callable]: + for tool in self.tools: + if tool.name == name: + return tool.tool + return None + + def list_tools(self) -> List[Dict[str, str]]: + return [{'name': t.name, 'description': t.description} for t in self.tools] + +# ActionCatalog class +def create_pydantic_models_for_ds_tool(func: Callable) -> Tuple[Type[BaseModel], Type[BaseModel]]: + """ + Dynamically create Pydantic models for the input and output of a function decorated with "@ds.tool". + + Parameters: + - func: The function to analyze and create models for. + + Returns: + - A tuple containing the original function, the input Pydantic model, and the output Pydantic model. + """ + # Extract the function signature + sig = inspect.signature(func) + input_fields = {} + for name, param in sig.parameters.items(): + # Determine the type of parameter, handling special types like Param and Secret + annotation = param.annotation + if hasattr(annotation, '__origin__') and annotation.__origin__ in [Param, Secret]: + # Extract the inner type and description from Param/Secret + field_type = annotation.__args__[0] + description = annotation.__metadata__[0] if annotation.__metadata__ else "" + default = param.default if param.default is not inspect.Parameter.empty else ... + input_fields[name] = (field_type, default, description) + else: + input_fields[name] = (param.annotation, param.default) + + # Create the input model dynamically + input_model = create_model(f"{func.__name__}Input", **input_fields) + + # Dynamically create the output model, handling complex return types with appropriate annotations + output_fields = {} + return_annotation = sig.return_annotation + if not return_annotation is inspect.Signature.empty: + if hasattr(return_annotation, '__args__'): # Check if it's a generic type (e.g., List[int]) + output_fields = {'result': (return_annotation.__args__[0], ...)} + else: + output_fields = {'result': (return_annotation, ...)} + output_model = create_model(f"{func.__name__}Output", **output_fields) + return input_model, output_model + diff --git a/toolserve/toolserve/server/core/conf.py b/toolserve/toolserve/server/core/conf.py new file mode 100644 index 00000000..5b461ece --- /dev/null +++ b/toolserve/toolserve/server/core/conf.py @@ -0,0 +1,101 @@ +import os + +from functools import lru_cache +from typing import Literal + +from pathlib import Path + +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + + +# https://docs.pydantic.dev/latest/concepts/pydantic_settings/ +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file='.env') + + WORK_DIR: Path = Path.home() / '.darkstar' + TOOLS_DIR: Path = os.getcwd() + + # Env Config + ENVIRONMENT: Literal['dev', 'pro'] = 'dev' + + # Env Redis + REDIS_HOST: str = 'localhost' + REDIS_PORT: int = 6379 + REDIS_PASSWORD: str = '' + REDIS_DATABASE: int = 0 + + # Env Token + TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32) + + # Env Opera Log + OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str + + # FastAPI + API_V1_STR: str = '/api/v1' + API_ACTION_STR: str = '/action' + TITLE: str = 'Darkstar Toolserver' + VERSION: str = '0.1.0' + DESCRIPTION: str = 'Darkstar Toolserver API' + DOCS_URL: str | None = f'{API_V1_STR}/docs' + REDOCS_URL: str | None = f'{API_V1_STR}/redocs' + OPENAPI_URL: str | None = f'{API_V1_STR}/openapi' + +# @model_validator(mode='before') +# @classmethod +# def validate_openapi_url(cls, values): +# if values['ENVIRONMENT'] == 'pro': +# values['OPENAPI_URL'] = None +# return values + + # Uvicorn + UVICORN_HOST: str = '127.0.0.1' + UVICORN_PORT: int = 8000 + UVICORN_RELOAD: bool = True + + # Static Server + STATIC_FILES: bool = False + + # DateTime + DATETIME_TIMEZONE: str = 'US/Pacific' + DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' + + # Redis + REDIS_TIMEOUT: int = 5 + + # Token + TOKEN_ALGORITHM: str = 'HS256' # 算法 + TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 + TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 刷新过期时间,单位:秒 + TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login' + TOKEN_REDIS_PREFIX: str = 'ts_token' + TOKEN_REFRESH_REDIS_PREFIX: str = 'ts_refresh_token' + TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 + f'{API_V1_STR}/auth/login', + ] + + # Log + LOG_STDOUT_FILENAME: str = 'ts_access.log' + LOG_STDERR_FILENAME: str = 'ts_error.log' + + # Middleware + MIDDLEWARE_CORS: bool = True + MIDDLEWARE_GZIP: bool = True + MIDDLEWARE_ACCESS: bool = False + + # these should be set in .env + TOKEN_SECRET_KEY: str = "secret" + OPERA_LOG_ENCRYPT_SECRET_KEY: str = "secret" + + + +@lru_cache +def get_settings(): + try: + env_path = Path(os.environ["TOOLSERVE_ENV"]) + except KeyError: + env_path = Path(__file__).parent.parent / '.env' + return Settings(_env_file=env_path) + +settings = get_settings() diff --git a/toolserve/toolserve/server/core/depends.py b/toolserve/toolserve/server/core/depends.py new file mode 100644 index 00000000..74e7bd1a --- /dev/null +++ b/toolserve/toolserve/server/core/depends.py @@ -0,0 +1,4 @@ +from starlette.requests import Request + +def get_catalog(request: Request): + return request.app.state.catalog \ No newline at end of file diff --git a/toolserve/toolserve/server/core/generate.py b/toolserve/toolserve/server/core/generate.py new file mode 100644 index 00000000..62fe6b7c --- /dev/null +++ b/toolserve/toolserve/server/core/generate.py @@ -0,0 +1,103 @@ +import os +import sys +import inspect +from textwrap import dedent +from typing import List, Optional, Type, Annotated, Dict +from pathlib import Path + +from fastapi import APIRouter, Body, Depends, Path, HTTPException +from pydantic import BaseModel, ValidationError, create_model +from importlib import import_module + +from toolserve.server.core.catalog import ToolSchema +from toolserve.server.core.conf import settings +from toolserve.common.response_code import CustomResponseCode +from toolserve.common.response import ResponseModel, response_base + + +def create_endpoint_function(name, description, func, input_model, output_model): + """ + Factory function to create endpoint functions with 'frozen' schema and input_model values. + """ + + async def run(body: input_model): + try: + # Execute the action + result = await func(**body.dict()) + valid_result = output_model(**result) + return await response_base.success(data=valid_result.dict()) + except ValidationError as e: + return await response_base.fail(res=CustomResponseCode.HTTP_400, msg=str(e)) + except Exception as e: + return await response_base.fail(res=CustomResponseCode.HTTP_500, msg=str(e)) + + run.__name__ = name + run.__doc__ = description + + return run + + +def create_response_model(name: str, output_model: Type[BaseModel]) -> Type[ResponseModel]: + """ + Create a response model for the given schema. + """ + # Create a new response model + response_model = create_model( + f"{name}Response", + code=(int, CustomResponseCode.HTTP_200.code), + msg=(str, CustomResponseCode.HTTP_200.msg), + data=(output_model, None) + ) + + return response_model + +def generate_endpoint(schemas: List[ToolSchema]) -> APIRouter: + routers = [] + top_level_router = APIRouter(prefix=settings.API_ACTION_STR) + + for schema in schemas: + router = APIRouter(prefix="/" + schema.meta.module) + + + # Create the endpoint function + run = create_endpoint_function( + name=schema.name, + description=schema.description, + func=schema.tool, + input_model=schema.input_model, + output_model=schema.output_model + ) + + response_model = create_response_model(schema.name, schema.output_model) + + # Add the endpoint to the FastAPI app + router.post( + f"/{schema.name}", + name=schema.name, + summary=schema.description, + tags=[schema.meta.module], + response_model=response_model, + response_description=create_output_description(schema.output_model) + )(run) + + routers.append(router) + for router in routers: + top_level_router.include_router(router) + return top_level_router + + + +def create_output_description(output_model: Type[BaseModel]) -> str: + """ + Create a description string for the output model. + """ + if not output_model: + return None + + output_description = dedent(output_model.__doc__ or "") + output_description += "\n\n**Attributes:**\n\n" + + for name, field in output_model.model_fields.items(): + output_description += f"- **{name}** ({field.annotation.__name__}): {field.description}\n" + + return output_description \ No newline at end of file diff --git a/toolserve/toolserve/server/core/registrar.py b/toolserve/toolserve/server/core/registrar.py new file mode 100644 index 00000000..36cb5617 --- /dev/null +++ b/toolserve/toolserve/server/core/registrar.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from contextlib import asynccontextmanager + +from fastapi import Depends, FastAPI + +from toolserve.server.routes import v1 +from toolserve.server.core.conf import settings +from toolserve.common.serializers import MsgSpecJSONResponse + +def register_app(): + # FastAPI + app = FastAPI( + title=settings.TITLE, + version=settings.VERSION, + description=settings.DESCRIPTION, + docs_url=settings.DOCS_URL, + redoc_url=settings.REDOCS_URL, + openapi_url=settings.OPENAPI_URL, + default_response_class=MsgSpecJSONResponse, + ) + + register_static_file(app) + + register_middleware(app) + + register_router(app) + + #register_exception(app) + + generate_actions_routers(app) + + return app + + +def register_static_file(app: FastAPI): + """ + + :param app: + :return: + """ + if settings.STATIC_FILES: + import os + + from fastapi.staticfiles import StaticFiles + + if not os.path.exists('./static'): + os.mkdir('./static') + app.mount('/static', StaticFiles(directory='static'), name='static') + + +def register_middleware(app: FastAPI): + """ + + :param app: + :return: + """ + # Gzip: Always at the top + if settings.MIDDLEWARE_GZIP: + from fastapi.middleware.gzip import GZipMiddleware + + app.add_middleware(GZipMiddleware) + + # CORS: Always at the end + if settings.MIDDLEWARE_CORS: + from fastapi.middleware.cors import CORSMiddleware + + app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], + ) + + +def register_router(app: FastAPI): + """ + 路由 + + :param app: FastAPI + :return: + """ + dependencies = None + + # API + app.include_router(v1, dependencies=dependencies) + + +def generate_actions_routers(app: FastAPI): + """ + + :param app: FastAPI + :return: + """ + from toolserve.server.core.generate import generate_endpoint + from toolserve.server.core.catalog import ToolCatalog + + catalog = ToolCatalog() + router = generate_endpoint(catalog.tools.values()) + app.include_router(router) + app.state.catalog = catalog \ No newline at end of file diff --git a/toolserve/toolserve/server/main.py b/toolserve/toolserve/server/main.py new file mode 100644 index 00000000..27c6fb8b --- /dev/null +++ b/toolserve/toolserve/server/main.py @@ -0,0 +1,23 @@ +import uvicorn + +from pathlib import Path + +from toolserve.common.log import log +from toolserve.server.core.conf import settings +from toolserve.server.core.registrar import register_app + +app = register_app() + +if __name__ == '__main__': + try: + log.info( + "Darkstar Toolserve is starting..." + ) + uvicorn.run( + app=f'{Path(__file__).stem}:app', + host=settings.UVICORN_HOST, + port=settings.UVICORN_PORT, + reload=settings.UVICORN_RELOAD, + ) + except Exception as e: + log.error(f'❌ FastAPI start filed: {e}') diff --git a/toolserve/toolserve/server/routes/__init__.py b/toolserve/toolserve/server/routes/__init__.py new file mode 100644 index 00000000..71332435 --- /dev/null +++ b/toolserve/toolserve/server/routes/__init__.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter +from toolserve.server.core.conf import settings +from toolserve.server.routes.action import router as action_router + +v1 = APIRouter(prefix=settings.API_V1_STR) +v1.include_router(action_router, tags=["action"]) diff --git a/toolserve/toolserve/server/routes/action.py b/toolserve/toolserve/server/routes/action.py new file mode 100644 index 00000000..39c78c3e --- /dev/null +++ b/toolserve/toolserve/server/routes/action.py @@ -0,0 +1,44 @@ +import os +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path, Query +from pydantic import ValidationError + +from toolserve.server.core.conf import settings +from toolserve.server.core.depends import get_catalog +from toolserve.common.response_code import CustomResponseCode +from toolserve.common.response import ResponseModel, response_base +#from toolserve.utils.openai_tool import schema_to_openai_tool + +router = APIRouter() + +@router.get( + '/list', + summary='List available tools', +) +async def list_tools(catalog=Depends(get_catalog)) -> ResponseModel: + """List all available actions""" + + tools = catalog.list_tools() + return await response_base.success(data=tools) + +@router.get( + '/oai_function', + summary="Get the OpenAI function format of an action" +) +async def get_oai_function( + action_name: str = Query(..., title="Action Name", description="The name of the action"), + catalog=Depends(get_catalog) +) -> ResponseModel: + """Get the OpenAI function format of an action""" + + try: + # TODO handle keyerror + action = catalog[action_name] + json_data = schema_to_openai_tool(action) + + return await response_base.success(data=json_data) + except ValidationError as e: + return await response_base.fail(res=CustomResponseCode.HTTP_400, data=str(e)) + except Exception as e: + return await response_base.fail(res=CustomResponseCode.HTTP_500, data=str(e)) diff --git a/toolserve/toolserve/server/routes/secret.py b/toolserve/toolserve/server/routes/secret.py new file mode 100644 index 00000000..e69de29b diff --git a/toolserve/toolserve/utils/__init__.py b/toolserve/toolserve/utils/__init__.py new file mode 100644 index 00000000..4fbf0713 --- /dev/null +++ b/toolserve/toolserve/utils/__init__.py @@ -0,0 +1,8 @@ +# Utility function to convert CamelCase to snake_case +def camel_to_snake(name): + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +def snake_to_camel(name): + return ''.join(x.capitalize() or '_' for x in name.split('_')) diff --git a/toolserve/toolserve/utils/openai_tool.py b/toolserve/toolserve/utils/openai_tool.py new file mode 100644 index 00000000..7fb34b62 --- /dev/null +++ b/toolserve/toolserve/utils/openai_tool.py @@ -0,0 +1,129 @@ +import json +from typing import Any, Dict, Type, Union, List +from pydantic import BaseModel, Field +from datetime import datetime + +from toolserve.server.core.catalog import ParameterSchema + +# TODO clean this up + +PYTHON_TO_JSON_TYPES = { + str: "string", + int: "integer", + float: "number", + bool: "boolean", + list: "array", + dict: "object", +} + +def python_type_to_json_type(python_type: Type) -> Dict[str, Any]: + """ + Map Python types to JSON Schema types, including handling of complex types such as lists and dictionaries. + + Args: + python_type (Type): The Python type to be converted to a JSON schema type. + + Returns: + Dict[str, Any]: A dictionary representing the JSON schema for the given Python type. + """ + if hasattr(python_type, '__origin__'): # For generic types like List[str] or Dict[str, int] + origin = python_type.__origin__ + if origin is list: + item_type = python_type_to_json_type(python_type.__args__[0]) + return {'type': 'array', 'items': {'type': item_type}} + elif origin is dict: + # Handle dictionary with specific key and value types + key_type = python_type_to_json_type(python_type.__args__[0]) + value_type = python_type_to_json_type(python_type.__args__[1]) + return {'type': 'object', 'additionalProperties': {'type': value_type}} + #elif issubclass(python_type, BaseModel): # For Pydantic models + # return model_to_json_schema(python_type) + return PYTHON_TO_JSON_TYPES.get(python_type, "string") + +def parameter_schema_to_json(parameter_schema: 'ParameterSchema') -> Dict[str, Any]: + """Convert a ParameterSchema to a JSON schema property.""" + property_schema = { + "type": python_type_to_json_type(parameter_schema.dtype), + "description": parameter_schema.description, + } + if parameter_schema.default is not None: + property_schema["default"] = parameter_schema.default + return property_schema + +def model_to_json_schema(model: Type[BaseModel]) -> Dict[str, Any]: + """Convert a Pydantic model to a JSON schema.""" + properties = {} + required = [] + for field_name, model_field in model.model_fields.items(): + field_schema = parameter_schema_to_json( + ParameterSchema( + name=field_name, + dtype=model_field.annotation, + description=model_field.description or "", + default=model_field.default, + required=model_field.required + ) + ) + properties[field_name] = field_schema + if model_field.is_required(): + required.append(field_name) + return { + "type": "object", + "properties": properties, + "required": required, + } + +def schema_to_openai_tool(action_schema: 'ActionSchema') -> str: + """Convert an ActionSchema object to a JSON schema string in the specified function format. + + Example output format: + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": ["celsius", "fahrenheit"] + } + }, + "required": ["location"] + } + } + } + + Args: + action_schema (ActionSchema): The action schema to convert. + + Returns: + str: A JSON schema string representing the action in the specified format. + """ + properties = {} + required = [] + if action_schema.in_schema: + for input_param in action_schema.in_schema.inputs: + param_schema = parameter_schema_to_json(input_param) + properties[input_param.name] = param_schema + if input_param.required: + required.append(input_param.name) + + function_schema = { + "type": "function", + "function": { + "name": action_schema.name, + "description": action_schema.description, + "parameters": { + "type": "object", + "properties": properties, + "required": required, + } + } + } + return json.dumps(function_schema, indent=2)