initial commit

This commit is contained in:
Sam Partee 2024-04-22 14:13:09 -07:00
commit 56531cbd18
39 changed files with 2245 additions and 0 deletions

10
.gitignore vendored Normal file
View file

@ -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/

View file

@ -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.')

BIN
examples/data/people.sqlite Normal file

Binary file not shown.

View file

@ -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
1 Name Age Location Occupation Email
2 Christopher Sharp 63 Los Angeles, CA Paediatric nurse grantanderson@simmons-jackson.com
3 Sharon Fernandez 69 San Diego, CA Cabin crew christysanders@white-love.net
4 Rachel Thornton 76 San Jose, CA Engineer, electrical douglas57@hotmail.com
5 Richard Moore 90 San Jose, CA Audiological scientist myersdaniel@williams-gutierrez.org
6 Kevin Fletcher 22 San Jose, CA Barrister's clerk hjohnson@hotmail.com
7 John Beasley 57 San Jose, CA Product/process development scientist thompsonpamela@gmail.com
8 Luis Anderson 88 San Antonio, TX Engineer, biomedical phale@gmail.com
9 Barbara Hodges 84 Dallas, TX Amenity horticulturist maciaskatherine@knight.com
10 Carla Weber 95 Chicago, IL Community pharmacist amycampbell@stanley.info
11 Sarah Little 75 Chicago, IL Horticultural consultant jenniferbrown@carroll.org
12 Jessica Greer 93 San Antonio, TX Engineer, broadcasting (operations) xnelson@day-williams.com
13 Brent Arnold 27 San Antonio, TX Lobbyist pjacobs@hotmail.com
14 Natalie Trujillo 59 Houston, TX Dramatherapist john40@ashley.com
15 Megan Gonzalez 87 Houston, TX Education officer, community jacqueline01@hotmail.com
16 Steven Davila 60 Dallas, TX Psychiatrist denisedominguez@hotmail.com
17 Joseph Parker 44 Los Angeles, CA Scientist, product/process development mendozamatthew@yahoo.com
18 Jennifer Travis 56 Dallas, TX Automotive engineer hbaker@gmail.com
19 Mr. Kenneth Phillips DVM 81 Dallas, TX Community arts worker kmorton@gmail.com
20 Dean Moore 98 Dallas, TX Engineer, site bushdenise@gmail.com
21 Dr. Margaret Ford 94 Philadelphia, PA Stage manager mclarke@hotmail.com
22 Jennifer Morrow 65 San Antonio, TX Best boy marshallrobert@hotmail.com
23 Robert Rivera 45 San Antonio, TX Call centre manager isaac50@russell-wagner.info
24 Tammy Powell 72 San Antonio, TX Editor, commissioning wsmith@hotmail.com
25 John Hughes 81 Phoenix, AZ Holiday representative cameron72@wright.info
26 Mary Smith 50 Phoenix, AZ Location manager bergdennis@yahoo.com
27 Laurie Burke 79 San Antonio, TX Event organiser steven32@yahoo.com
28 Kevin Smith 49 Dallas, TX Accommodation manager boydhannah@yahoo.com
29 John Foster 80 Houston, TX Field seismologist garrettmartin@dixon.com
30 Ashlee Charles 63 San Jose, CA Editor, commissioning rholmes@vega.info
31 Cynthia Potts 34 New York, NY Lecturer, higher education ulang@vasquez.org
32 Jesus Brady 61 Dallas, TX Forensic scientist gonzalezbrenda@gmail.com
33 Jamie Smith 59 Philadelphia, PA Retail buyer taylorthomas@soto.com
34 Abigail Hicks 34 Chicago, IL Therapist, speech and language lori49@gmail.com
35 Thomas Rodriguez 63 San Jose, CA Retail banker erica79@glover-forbes.com
36 Eric Wilkinson 75 San Antonio, TX Art gallery manager pnixon@callahan.com
37 Matthew Cantu 39 Los Angeles, CA Chief Financial Officer reynoldsgregory@gmail.com
38 Chloe Chapman 35 Philadelphia, PA Production manager bdavis@hotmail.com
39 Sophia Jones 48 San Jose, CA Camera operator gsantos@gmail.com
40 David Young 56 Houston, TX Psychologist, clinical leslie55@hotmail.com
41 Stacy Morris 72 Los Angeles, CA Logistics and distribution manager qhansen@yahoo.com
42 Julie Lin 56 Los Angeles, CA Social worker gzimmerman@gmail.com
43 Melinda White 92 Philadelphia, PA Probation officer pamela45@yahoo.com
44 Elizabeth Daniel 95 Dallas, TX Sports coach lauriegriffin@fletcher-garcia.com
45 Stephanie Fuller 86 San Antonio, TX Heritage manager benjamin88@gmail.com
46 Peter Salinas 32 Philadelphia, PA Building control surveyor charles57@martin-lane.com
47 Michael Hart 83 Dallas, TX Purchasing manager john23@hotmail.com
48 Roger Oconnor 75 Chicago, IL Arts administrator heather19@yahoo.com
49 John Stanley 25 San Jose, CA Training and development officer kyle24@gmail.com
50 William Hatfield 39 Los Angeles, CA Adult guidance worker ajohnson@hoover.net
51 Brent Stout 72 San Diego, CA Fish farm manager marcus27@richardson-jones.com
52 Mitchell Jackson 66 New York, NY Quantity surveyor nclark@hotmail.com
53 Jake Davila 28 San Diego, CA Quality manager victorcastro@yahoo.com
54 Doris Hodge 72 San Antonio, TX Computer games developer watersscott@hotmail.com
55 Erica Harris 88 San Diego, CA Secondary school teacher achurch@burton-mejia.com
56 Joseph Hansen 59 Dallas, TX Medical secretary olsenricardo@yahoo.com
57 Barbara Stout 72 San Jose, CA Clinical biochemist victoriabrewer@allen.com
58 Sandra Gonzalez 73 Chicago, IL Engineer, maintenance james02@snyder.com
59 Matthew Jones 43 Philadelphia, PA Electronics engineer craigperez@moore.biz
60 Terry Nichols 93 Philadelphia, PA Outdoor activities/education manager rogersnicole@chang-stanley.com
61 James Berry 94 San Jose, CA Buyer, industrial serranochristopher@harrison-williams.org
62 Angela Sandoval 71 Phoenix, AZ Optician, dispensing hurleyjennifer@williams.org
63 John Hawkins 38 San Antonio, TX Programmer, systems johnsondonna@miller.com
64 Thomas Cooper 45 San Antonio, TX Advertising art director gonzalezjeffrey@nelson-reynolds.com
65 Olivia Owens 86 San Diego, CA Buyer, retail robertsontyler@edwards-hansen.com
66 Kelli Rodriguez 34 Philadelphia, PA Therapist, horticultural tara21@gmail.com
67 Jeanette Briggs 61 Philadelphia, PA Tourism officer alexander42@jones.com
68 Kimberly Perry 71 Dallas, TX Firefighter conwaymichelle@duran.org
69 Jorge Brandt 74 Phoenix, AZ Programmer, systems veronicafigueroa@weber-johnson.com
70 Brent Hayes 71 New York, NY Conservator, furniture sgallagher@cook-marshall.net
71 Julie Blake 99 San Diego, CA Colour technologist chriswilson@hawkins.com
72 David Jackson MD 33 New York, NY Arts administrator melissa67@davila-johnson.com
73 Anna Nunez 28 Dallas, TX Plant breeder/geneticist lhernandez@rodriguez.org
74 Caleb Adams 38 San Diego, CA Podiatrist ugonzales@mitchell-lopez.info
75 Morgan Haas 88 San Jose, CA Toxicologist michael10@yahoo.com
76 Emily Baker 22 Phoenix, AZ Equities trader bherring@hotmail.com
77 Madison Edwards 33 Houston, TX Field seismologist shawn27@webb.com
78 Edwin Conley 63 San Jose, CA Communications engineer christinatapia@mendez-hughes.com
79 Meredith Randall 57 Los Angeles, CA Exercise physiologist andrewneal@yahoo.com
80 George Ramos 95 New York, NY Energy engineer charles02@hotmail.com
81 Harry Watts 77 San Diego, CA Copywriter, advertising david08@warner.com
82 Lisa Lee 77 Philadelphia, PA Tourism officer alishapratt@gmail.com
83 John Watson 19 Houston, TX Chief Executive Officer robynhernandez@gmail.com
84 Joseph Sanchez 20 Chicago, IL Phytotherapist allisonlauren@mcintyre.com
85 Seth Young 18 Chicago, IL Economist elizabethbrown@hall-heath.com
86 Luis Gentry 55 Houston, TX Technical author cpowers@williams.org
87 Dawn Jones 39 Dallas, TX Designer, television/film set amber85@jackson-lee.com
88 David Davila 81 Los Angeles, CA Adult nurse melissawilson@cantrell-reyes.org
89 Amy Donaldson 49 San Diego, CA Computer games developer abigailgomez@gmail.com
90 Christine Burke 84 Philadelphia, PA Energy manager obrown@smith-mckinney.org
91 Krista Gordon 83 Chicago, IL Computer games developer kelleynicole@grant.com
92 Vanessa Gibson 67 Chicago, IL Armed forces operational officer nicholasreid@wright-jones.com
93 Wendy Palmer 47 Houston, TX Engineer, maintenance (IT) ymeyer@hotmail.com
94 Alicia Bass 86 Houston, TX Applications developer joneslinda@adams-conley.com
95 Jason Lyons 33 Philadelphia, PA Newspaper journalist kimberlysharp@ballard.info
96 Travis Cohen 98 San Antonio, TX Music tutor drakejohn@castro.info
97 Miss Regina Bullock 72 Houston, TX Fisheries officer amymercer@hotmail.com
98 Kevin Johnson 80 San Antonio, TX Futures trader sampsonkimberly@hotmail.com
99 Mark Bailey 66 San Jose, CA Programmer, applications nanderson@west-barajas.com
100 Dustin Clark 91 Dallas, TX Teacher, early years/pre barrettjohn@miller.com
101 Autumn Reed 29 Phoenix, AZ Ophthalmologist hannah08@hotmail.com

View file

@ -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")

View file

@ -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"

11
examples/gmail/pack.toml Normal file
View file

@ -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"

View file

@ -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()

0
toolserve/README.md Normal file
View file

663
toolserve/poetry.lock generated Normal file
View file

@ -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"

30
toolserve/pyproject.toml Normal file
View file

@ -0,0 +1,30 @@
[tool.poetry]
name = "toolserve"
version = "0.1.0"
description = ""
authors = ["Sam Partee <Partees21@gmail.com>"]
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"

View file

View file

View file

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

View file

@ -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)

View file

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,6 @@
from .tool import (
Param,
tool,
Secret
)

View file

@ -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

View file

View file

@ -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

View file

@ -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()

View file

@ -0,0 +1,4 @@
from starlette.requests import Request
def get_catalog(request: Request):
return request.app.state.catalog

View file

@ -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

View file

@ -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

View file

@ -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}')

View file

@ -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"])

View file

@ -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))

View file

@ -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('_'))

View file

@ -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)