diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml
index e013616db..a09fed4c8 100644
--- a/autogpt_platform/docker-compose.platform.yml
+++ b/autogpt_platform/docker-compose.platform.yml
@@ -144,6 +144,51 @@ services:
networks:
- app-network
+ market:
+ build:
+ context: ../
+ dockerfile: autogpt_platform/market/Dockerfile
+ develop:
+ watch:
+ - path: ./
+ target: autogpt_platform/market/
+ action: rebuild
+ depends_on:
+ db:
+ condition: service_healthy
+ market-migrations:
+ condition: service_completed_successfully
+ environment:
+ - SUPABASE_URL=http://kong:8000
+ - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
+ - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
+ - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
+ - BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
+ ports:
+ - "8015:8015"
+ networks:
+ - app-network
+
+ market-migrations:
+ build:
+ context: ../
+ dockerfile: autogpt_platform/market/Dockerfile
+ command: ["sh", "-c", "poetry run prisma migrate deploy"]
+ develop:
+ watch:
+ - path: ./
+ target: autogpt_platform/market/
+ action: rebuild
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ - SUPABASE_URL=http://kong:8000
+ - SUPABASE_JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
+ - SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
+ - DATABASE_URL=postgresql://postgres:your-super-secret-and-long-postgres-password@db:5432/postgres?connect_timeout=60&schema=market
+ networks:
+ - app-network
# frontend:
# build:
# context: ../
diff --git a/autogpt_platform/docker-compose.yml b/autogpt_platform/docker-compose.yml
index 9166e0e32..d9cafccf0 100644
--- a/autogpt_platform/docker-compose.yml
+++ b/autogpt_platform/docker-compose.yml
@@ -51,6 +51,18 @@ services:
file: ./docker-compose.platform.yml
service: websocket_server
+ market:
+ <<: *agpt-services
+ extends:
+ file: ./docker-compose.platform.yml
+ service: market
+
+ market-migrations:
+ <<: *agpt-services
+ extends:
+ file: ./docker-compose.platform.yml
+ service: market-migrations
+
# frontend:
# <<: *agpt-services
# extends:
diff --git a/autogpt_platform/market/.env.example b/autogpt_platform/market/.env.example
new file mode 100644
index 000000000..b3b1cb8a4
--- /dev/null
+++ b/autogpt_platform/market/.env.example
@@ -0,0 +1,12 @@
+DB_USER=postgres
+DB_PASS=your-super-secret-and-long-postgres-password
+DB_NAME=postgres
+DB_PORT=5432
+DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}?connect_timeout=60&schema=market"
+SENTRY_DSN=https://11d0640fef35640e0eb9f022eb7d7626@o4505260022104064.ingest.us.sentry.io/4507890252447744
+
+ENABLE_AUTH=true
+SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long
+BACKEND_CORS_ALLOW_ORIGINS="http://localhost:3000,http://127.0.0.1:3000"
+
+APP_ENV=local
diff --git a/autogpt_platform/market/.gitignore b/autogpt_platform/market/.gitignore
new file mode 100644
index 000000000..7fd0341ba
--- /dev/null
+++ b/autogpt_platform/market/.gitignore
@@ -0,0 +1,6 @@
+database.db
+database.db-journal
+build/
+config.json
+secrets/*
+!secrets/.gitkeep
\ No newline at end of file
diff --git a/autogpt_platform/market/Dockerfile b/autogpt_platform/market/Dockerfile
new file mode 100644
index 000000000..dbd12565d
--- /dev/null
+++ b/autogpt_platform/market/Dockerfile
@@ -0,0 +1,72 @@
+FROM python:3.11.10-slim-bookworm AS builder
+
+# Set environment variables
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+WORKDIR /app
+
+RUN echo 'Acquire::http::Pipeline-Depth 0;\nAcquire::http::No-Cache true;\nAcquire::BrokenProxy true;\n' > /etc/apt/apt.conf.d/99fixbadproxy
+
+RUN apt-get update --allow-releaseinfo-change --fix-missing
+
+# Install build dependencies
+RUN apt-get install -y build-essential
+RUN apt-get install -y libpq5
+RUN apt-get install -y libz-dev
+RUN apt-get install -y libssl-dev
+
+ENV POETRY_VERSION=1.8.3 \
+ POETRY_HOME="/opt/poetry" \
+ POETRY_NO_INTERACTION=1 \
+ POETRY_VIRTUALENVS_CREATE=false
+ENV PATH="$POETRY_HOME/bin:$PATH"
+
+# Upgrade pip and setuptools to fix security vulnerabilities
+RUN pip3 install --upgrade pip setuptools
+
+RUN pip3 install poetry
+
+# Copy and install dependencies
+COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
+COPY autogpt_platform/market/poetry.lock autogpt_platform/market/pyproject.toml /app/autogpt_platform/market/
+WORKDIR /app/autogpt_platform/market
+RUN poetry config virtualenvs.create false \
+ && poetry install --no-interaction --no-ansi
+
+# Generate Prisma client
+COPY autogpt_platform/market /app/autogpt_platform/market
+RUN poetry config virtualenvs.create false \
+ && poetry run prisma generate
+
+FROM python:3.11.10-slim-bookworm AS server_dependencies
+
+WORKDIR /app
+
+# Upgrade pip and setuptools to fix security vulnerabilities
+RUN pip3 install --upgrade pip setuptools
+
+# Copy only necessary files from builder
+COPY --from=builder /app /app
+COPY --from=builder /usr/local/lib/python3.11 /usr/local/lib/python3.11
+COPY --from=builder /usr/local/bin /usr/local/bin
+# Copy Prisma binaries
+COPY --from=builder /root/.cache/prisma-python/binaries /root/.cache/prisma-python/binaries
+
+ENV PATH="/app/.venv/bin:$PATH"
+
+RUN mkdir -p /app/autogpt_platform/autogpt_libs
+RUN mkdir -p /app/autogpt_platform/market
+
+COPY autogpt_platform/autogpt_libs /app/autogpt_platform/autogpt_libs
+
+COPY autogpt_platform/market /app/autogpt_platform/market
+
+WORKDIR /app/autogpt_platform/market
+
+FROM server_dependencies AS server
+
+ENV DATABASE_URL=""
+ENV PORT=8015
+
+CMD ["poetry", "run", "app"]
diff --git a/autogpt_platform/market/README.md b/autogpt_platform/market/README.md
new file mode 100644
index 000000000..a799f362b
--- /dev/null
+++ b/autogpt_platform/market/README.md
@@ -0,0 +1,37 @@
+# AutoGPT Agent Marketplace
+
+## Overview
+
+AutoGPT Agent Marketplace is an open-source platform for autonomous AI agents. This project aims to create a user-friendly, accessible marketplace where users can discover, utilize, and contribute to a diverse ecosystem of AI solutions.
+
+## Vision
+
+Our vision is to empower users with customizable and free AI agents, fostering an open-source community that drives innovation in AI automation across various industries.
+
+## Key Features
+
+- Agent Discovery and Search
+- Agent Listings with Detailed Information
+- User Profiles
+- Data Protection and Compliance
+
+## Getting Started
+
+To get started with the AutoGPT Agent Marketplace, follow these steps:
+
+- Copy `.env.example` to `.env` and fill in the required environment variables
+- Run `poetry run setup`
+- Run `poetry run populate`
+- Run `poetry run app`
+
+## Poetry Run Commands
+
+This section outlines the available command line scripts for this project, configured using Poetry. You can execute these scripts directly using Poetry. Each command performs a specific operation as described below:
+
+- `poetry run format`: Runs the formatting script to ensure code consistency.
+- `poetry run lint`: Executes the linting script to identify and fix potential code issues.
+- `poetry run app`: Starts the main application.
+- `poetry run setup`: Runs the setup script to configure the database.
+- `poetry run populate`: Populates the database with initial data using the specified script.
+
+To run any of these commands, ensure Poetry is installed on your system and execute the commands from the project's root directory.
diff --git a/autogpt_platform/market/docker-compose.yml b/autogpt_platform/market/docker-compose.yml
new file mode 100644
index 000000000..3eaa40892
--- /dev/null
+++ b/autogpt_platform/market/docker-compose.yml
@@ -0,0 +1,16 @@
+version: "3"
+services:
+ postgres:
+ image: ankane/pgvector:latest
+ environment:
+ POSTGRES_USER: ${DB_USER}
+ POSTGRES_PASSWORD: ${DB_PASS}
+ POSTGRES_DB: ${DB_NAME}
+ PGUSER: ${DB_USER}
+ healthcheck:
+ test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ ports:
+ - "${DB_PORT}:5432"
diff --git a/autogpt_platform/market/market/__init__.py b/autogpt_platform/market/market/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/autogpt_platform/market/market/app.py b/autogpt_platform/market/market/app.py
new file mode 100644
index 000000000..63736acd3
--- /dev/null
+++ b/autogpt_platform/market/market/app.py
@@ -0,0 +1,97 @@
+import contextlib
+import logging.config
+import os
+
+import dotenv
+import fastapi
+import fastapi.middleware.cors
+import fastapi.middleware.gzip
+import prisma
+import prometheus_fastapi_instrumentator
+import sentry_sdk
+import sentry_sdk.integrations.asyncio
+import sentry_sdk.integrations.fastapi
+import sentry_sdk.integrations.starlette
+
+import market.config
+import market.routes.admin
+import market.routes.agents
+import market.routes.analytics
+import market.routes.search
+import market.routes.submissions
+
+dotenv.load_dotenv()
+
+logging.config.dictConfig(market.config.LogConfig().model_dump())
+
+if os.environ.get("SENTRY_DSN"):
+ sentry_sdk.init(
+ dsn=os.environ.get("SENTRY_DSN"),
+ traces_sample_rate=1.0,
+ profiles_sample_rate=1.0,
+ enable_tracing=True,
+ environment=os.environ.get("RUN_ENV", default="CLOUD").lower(),
+ integrations=[
+ sentry_sdk.integrations.starlette.StarletteIntegration(
+ transaction_style="url"
+ ),
+ sentry_sdk.integrations.fastapi.FastApiIntegration(transaction_style="url"),
+ sentry_sdk.integrations.asyncio.AsyncioIntegration(),
+ ],
+ )
+
+db_client = prisma.Prisma(auto_register=True)
+
+
+@contextlib.asynccontextmanager
+async def lifespan(app: fastapi.FastAPI):
+ await db_client.connect()
+ yield
+ await db_client.disconnect()
+
+
+docs_url = "/docs"
+app = fastapi.FastAPI(
+ title="Marketplace API",
+ description="AutoGPT Marketplace API is a service that allows users to share AI agents.",
+ summary="Maketplace API",
+ version="0.1",
+ lifespan=lifespan,
+ root_path="/api/v1/market",
+ docs_url=docs_url,
+)
+
+app.add_middleware(fastapi.middleware.gzip.GZipMiddleware, minimum_size=1000)
+app.add_middleware(
+ middleware_class=fastapi.middleware.cors.CORSMiddleware,
+ allow_origins=os.environ.get(
+ "BACKEND_CORS_ALLOW_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000"
+ ).split(","),
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+app.include_router(market.routes.agents.router, tags=["agents"])
+app.include_router(market.routes.search.router, tags=["search"])
+app.include_router(market.routes.submissions.router, tags=["submissions"])
+app.include_router(market.routes.admin.router, prefix="/admin", tags=["admin"])
+app.include_router(
+ market.routes.analytics.router, prefix="/analytics", tags=["analytics"]
+)
+
+
+@app.get("/health")
+def health():
+ return fastapi.responses.HTMLResponse(
+ content="
Marketplace API
", status_code=200
+ )
+
+
+@app.get("/")
+def default():
+ return fastapi.responses.HTMLResponse(
+ content="Marketplace API
", status_code=200
+ )
+
+
+prometheus_fastapi_instrumentator.Instrumentator().instrument(app).expose(app)
diff --git a/autogpt_platform/market/market/config.py b/autogpt_platform/market/market/config.py
new file mode 100644
index 000000000..46a31b462
--- /dev/null
+++ b/autogpt_platform/market/market/config.py
@@ -0,0 +1,30 @@
+from pydantic import BaseModel
+
+
+class LogConfig(BaseModel):
+ """Logging configuration to be set for the server"""
+
+ LOGGER_NAME: str = "marketplace"
+ LOG_FORMAT: str = "%(levelprefix)s | %(asctime)s | %(message)s"
+ LOG_LEVEL: str = "DEBUG"
+
+ # Logging config
+ version: int = 1
+ disable_existing_loggers: bool = False
+ formatters: dict = {
+ "default": {
+ "()": "uvicorn.logging.DefaultFormatter",
+ "fmt": LOG_FORMAT,
+ "datefmt": "%Y-%m-%d %H:%M:%S",
+ },
+ }
+ handlers: dict = {
+ "default": {
+ "formatter": "default",
+ "class": "logging.StreamHandler",
+ "stream": "ext://sys.stderr",
+ },
+ }
+ loggers: dict = {
+ LOGGER_NAME: {"handlers": ["default"], "level": LOG_LEVEL},
+ }
diff --git a/autogpt_platform/market/market/db.py b/autogpt_platform/market/market/db.py
new file mode 100644
index 000000000..6b4418bd8
--- /dev/null
+++ b/autogpt_platform/market/market/db.py
@@ -0,0 +1,725 @@
+import datetime
+import typing
+
+import fuzzywuzzy.fuzz
+import prisma.enums
+import prisma.errors
+import prisma.models
+import prisma.types
+import pydantic
+
+import market.model
+import market.utils.extension_types
+
+
+class AgentQueryError(Exception):
+ """Custom exception for agent query errors"""
+
+ pass
+
+
+class TopAgentsDBResponse(pydantic.BaseModel):
+ """
+ Represents a response containing a list of top agents.
+
+ Attributes:
+ analytics (list[AgentResponse]): The list of top agents.
+ total_count (int): The total count of agents.
+ page (int): The current page number.
+ page_size (int): The number of agents per page.
+ total_pages (int): The total number of pages.
+ """
+
+ analytics: list[prisma.models.AnalyticsTracker]
+ total_count: int
+ page: int
+ page_size: int
+ total_pages: int
+
+
+class FeaturedAgentResponse(pydantic.BaseModel):
+ """
+ Represents a response containing a list of featured agents.
+
+ Attributes:
+ featured_agents (list[FeaturedAgent]): The list of featured agents.
+ total_count (int): The total count of featured agents.
+ page (int): The current page number.
+ page_size (int): The number of agents per page.
+ total_pages (int): The total number of pages.
+ """
+
+ featured_agents: list[prisma.models.FeaturedAgent]
+ total_count: int
+ page: int
+ page_size: int
+ total_pages: int
+
+
+async def delete_agent(agent_id: str) -> prisma.models.Agents | None:
+ """
+ Delete an agent from the database.
+
+ Args:
+ agent_id (str): The ID of the agent to delete.
+
+ Returns:
+ prisma.models.Agents | None: The deleted agent if found, None otherwise.
+
+ Raises:
+ AgentQueryError: If there is an error deleting the agent from the database.
+ """
+ try:
+ deleted_agent = await prisma.models.Agents.prisma().delete(
+ where={"id": agent_id}
+ )
+ return deleted_agent
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def create_agent_entry(
+ name: str,
+ description: str,
+ author: str,
+ keywords: typing.List[str],
+ categories: typing.List[str],
+ graph: prisma.Json,
+ submission_state: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.PENDING,
+):
+ """
+ Create a new agent entry in the database.
+
+ Args:
+ name (str): The name of the agent.
+ description (str): The description of the agent.
+ author (str): The author of the agent.
+ keywords (List[str]): The keywords associated with the agent.
+ categories (List[str]): The categories associated with the agent.
+ graph (dict): The graph data of the agent.
+
+ Returns:
+ dict: The newly created agent entry.
+
+ Raises:
+ AgentQueryError: If there is an error creating the agent entry.
+ """
+ try:
+ agent = await prisma.models.Agents.prisma().create(
+ data={
+ "name": name,
+ "description": description,
+ "author": author,
+ "keywords": keywords,
+ "categories": categories,
+ "graph": graph,
+ "AnalyticsTracker": {"create": {"downloads": 0, "views": 0}},
+ "submissionStatus": submission_state,
+ }
+ )
+
+ return agent
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def update_agent_entry(
+ agent_id: str,
+ version: int,
+ submission_state: prisma.enums.SubmissionStatus,
+ comments: str | None = None,
+) -> prisma.models.Agents | None:
+ """
+ Update an existing agent entry in the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ version (int): The version of the agent.
+ submission_state (prisma.enums.SubmissionStatus): The submission state of the agent.
+ """
+
+ try:
+ agent = await prisma.models.Agents.prisma().update(
+ where={"id": agent_id},
+ data={
+ "version": version,
+ "submissionStatus": submission_state,
+ "submissionReviewDate": datetime.datetime.now(datetime.timezone.utc),
+ "submissionReviewComments": comments,
+ },
+ )
+
+ return agent
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Agent Update Failed Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_agents(
+ page: int = 1,
+ page_size: int = 10,
+ name: str | None = None,
+ keyword: str | None = None,
+ category: str | None = None,
+ description: str | None = None,
+ description_threshold: int = 60,
+ submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
+ sort_by: str = "createdAt",
+ sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
+):
+ """
+ Retrieve a list of agents from the database based on the provided filters and pagination parameters.
+
+ Args:
+ page (int, optional): The page number to retrieve. Defaults to 1.
+ page_size (int, optional): The number of agents per page. Defaults to 10.
+ name (str, optional): Filter agents by name. Defaults to None.
+ keyword (str, optional): Filter agents by keyword. Defaults to None.
+ category (str, optional): Filter agents by category. Defaults to None.
+ description (str, optional): Filter agents by description. Defaults to None.
+ description_threshold (int, optional): The minimum fuzzy search threshold for the description. Defaults to 60.
+ sort_by (str, optional): The field to sort the agents by. Defaults to "createdAt".
+ sort_order (str, optional): The sort order ("asc" or "desc"). Defaults to "desc".
+
+ Returns:
+ dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
+ """
+ try:
+ # Define the base query
+ query = {}
+
+ # Add optional filters
+ if name:
+ query["name"] = {"contains": name, "mode": "insensitive"}
+ if keyword:
+ query["keywords"] = {"has": keyword}
+ if category:
+ query["categories"] = {"has": category}
+
+ query["submissionStatus"] = submission_status
+
+ # Define sorting
+ order = {sort_by: sort_order}
+
+ # Calculate pagination
+ skip = (page - 1) * page_size
+
+ # Execute the query
+ try:
+ agents = await prisma.models.Agents.prisma().find_many(
+ where=query, # type: ignore
+ order=order, # type: ignore
+ skip=skip,
+ take=page_size,
+ )
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+
+ # Apply fuzzy search on description if provided
+ if description:
+ try:
+ filtered_agents = []
+ for agent in agents:
+ if (
+ agent.description
+ and fuzzywuzzy.fuzz.partial_ratio(
+ description.lower(), agent.description.lower()
+ )
+ >= description_threshold
+ ):
+ filtered_agents.append(agent)
+ agents = filtered_agents
+ except AttributeError as e:
+ raise AgentQueryError(f"Error during fuzzy search: {str(e)}")
+
+ # Get total count for pagination info
+ total_count = len(agents)
+
+ return {
+ "agents": agents,
+ "total_count": total_count,
+ "page": page,
+ "page_size": page_size,
+ "total_pages": (total_count + page_size - 1) // page_size,
+ }
+
+ except AgentQueryError as e:
+ # Log the error or handle it as needed
+ raise e
+ except ValueError as e:
+ raise AgentQueryError(f"Invalid input parameter: {str(e)}")
+ except Exception as e:
+ # Catch any other unexpected exceptions
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_agent_details(agent_id: str, version: int | None = None):
+ """
+ Retrieve agent details from the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ version (int | None, optional): The version of the agent. Defaults to None.
+
+ Returns:
+ dict: The agent details.
+
+ Raises:
+ AgentQueryError: If the agent is not found or if there is an error querying the database.
+ """
+ try:
+ query = {"id": agent_id}
+ if version is not None:
+ query["version"] = version # type: ignore
+
+ agent = await prisma.models.Agents.prisma().find_first(where=query) # type: ignore
+
+ if not agent:
+ raise AgentQueryError("Agent not found")
+
+ return agent
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def search_db(
+ query: str,
+ page: int = 1,
+ page_size: int = 10,
+ categories: typing.List[str] | None = None,
+ description_threshold: int = 60,
+ sort_by: str = "rank",
+ sort_order: typing.Literal["desc"] | typing.Literal["asc"] = "desc",
+ submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
+) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
+ """Perform a search for agents based on the provided query string.
+
+ Args:
+ query (str): the search string
+ page (int, optional): page for searching. Defaults to 1.
+ page_size (int, optional): the number of results to return. Defaults to 10.
+ categories (List[str] | None, optional): list of category filters. Defaults to None.
+ description_threshold (int, optional): number of characters to return. Defaults to 60.
+ sort_by (str, optional): sort by option. Defaults to "rank".
+ sort_order ("asc" | "desc", optional): the sort order. Defaults to "desc".
+
+ Raises:
+ AgentQueryError: Raises an error if the query fails.
+ AgentQueryError: Raises if an unexpected error occurs.
+
+ Returns:
+ List[AgentsWithRank]: List of agents matching the search criteria.
+ """
+ try:
+ offset = (page - 1) * page_size
+
+ category_filter = "1=1"
+ if categories:
+ category_conditions = [f"'{cat}' = ANY(categories)" for cat in categories]
+ category_filter = "AND (" + " OR ".join(category_conditions) + ")"
+
+ # Construct the ORDER BY clause based on the sort_by parameter
+ if sort_by in ["createdAt", "updatedAt"]:
+ order_by_clause = f'"{sort_by}" {sort_order.upper()}, rank DESC'
+ elif sort_by == "name":
+ order_by_clause = f"name {sort_order.upper()}, rank DESC"
+ else:
+ order_by_clause = 'rank DESC, "createdAt" DESC'
+
+ submission_status_filter = f""""submissionStatus" = '{submission_status}'"""
+
+ sql_query = f"""
+ WITH query AS (
+ SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
+ FROM unnest(to_tsvector('{query}'))
+ )
+ SELECT
+ id,
+ "createdAt",
+ "updatedAt",
+ version,
+ name,
+ LEFT(description, {description_threshold}) AS description,
+ author,
+ keywords,
+ categories,
+ graph,
+ "submissionStatus",
+ "submissionDate",
+ CASE
+ WHEN query.q::text = '' THEN 1.0
+ ELSE COALESCE(ts_rank(CAST(search AS tsvector), query.q), 0.0)
+ END AS rank
+ FROM market."Agents", query
+ WHERE
+ (query.q::text = '' OR search @@ query.q)
+ AND {category_filter}
+ AND {submission_status_filter}
+ ORDER BY {order_by_clause}
+ LIMIT {page_size}
+ OFFSET {offset};
+ """
+
+ results = await prisma.client.get_client().query_raw(
+ query=sql_query,
+ model=market.utils.extension_types.AgentsWithRank,
+ )
+
+ class CountResponse(pydantic.BaseModel):
+ count: int
+
+ count_query = f"""
+ WITH query AS (
+ SELECT to_tsquery(string_agg(lexeme || ':*', ' & ' ORDER BY positions)) AS q
+ FROM unnest(to_tsvector('{query}'))
+ )
+ SELECT COUNT(*)
+ FROM market."Agents", query
+ WHERE (search @@ query.q OR query.q = '') AND {category_filter} AND {submission_status_filter};
+ """
+
+ total_count = await prisma.client.get_client().query_first(
+ query=count_query,
+ model=CountResponse,
+ )
+ total_count = total_count.count if total_count else 0
+
+ return market.model.ListResponse(
+ items=results,
+ total_count=total_count,
+ page=page,
+ page_size=page_size,
+ total_pages=(total_count + page_size - 1) // page_size,
+ )
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_top_agents_by_downloads(
+ page: int = 1,
+ page_size: int = 10,
+ submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
+) -> market.model.ListResponse[prisma.models.AnalyticsTracker]:
+ """Retrieve the top agents by download count.
+
+ Args:
+ page (int, optional): The page number. Defaults to 1.
+ page_size (int, optional): The number of agents per page. Defaults to 10.
+
+ Returns:
+ dict: A dictionary containing the list of agents, total count, current page number, page size, and total number of pages.
+ """
+ try:
+ # Calculate pagination
+ skip = (page - 1) * page_size
+
+ # Execute the query
+ try:
+ # Agents with no downloads will not be included in the results... is this the desired behavior?
+ analytics = await prisma.models.AnalyticsTracker.prisma().find_many(
+ include={"agent": True},
+ order={"downloads": "desc"},
+ where={"agent": {"is": {"submissionStatus": submission_status}}},
+ skip=skip,
+ take=page_size,
+ )
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+
+ try:
+ total_count = await prisma.models.AnalyticsTracker.prisma().count(
+ where={"agent": {"is": {"submissionStatus": submission_status}}},
+ )
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+
+ return market.model.ListResponse(
+ items=analytics,
+ total_count=total_count,
+ page=page,
+ page_size=page_size,
+ total_pages=(total_count + page_size - 1) // page_size,
+ )
+
+ except AgentQueryError as e:
+ # Log the error or handle it as needed
+ raise e from e
+ except ValueError as e:
+ raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
+ except Exception as e:
+ # Catch any other unexpected exceptions
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
+
+
+async def set_agent_featured(
+ agent_id: str, is_active: bool = True, featured_categories: list[str] = ["featured"]
+) -> prisma.models.FeaturedAgent:
+ """Set an agent as featured in the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ category (str, optional): The category to set the agent as featured. Defaults to "featured".
+
+ Raises:
+ AgentQueryError: If there is an error setting the agent as featured.
+ """
+ try:
+ agent = await prisma.models.Agents.prisma().find_unique(where={"id": agent_id})
+ if not agent:
+ raise AgentQueryError(f"Agent with ID {agent_id} not found.")
+
+ featured = await prisma.models.FeaturedAgent.prisma().upsert(
+ where={"agentId": agent_id},
+ data={
+ "update": {
+ "featuredCategories": featured_categories,
+ "isActive": is_active,
+ },
+ "create": {
+ "featuredCategories": featured_categories,
+ "isActive": is_active,
+ "agent": {"connect": {"id": agent_id}},
+ },
+ },
+ )
+ return featured
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_featured_agents(
+ category: str = "featured",
+ page: int = 1,
+ page_size: int = 10,
+ submission_status: prisma.enums.SubmissionStatus = prisma.enums.SubmissionStatus.APPROVED,
+) -> FeaturedAgentResponse:
+ """Retrieve a list of featured agents from the database based on the provided category.
+
+ Args:
+ category (str, optional): The category of featured agents to retrieve. Defaults to "featured".
+ page (int, optional): The page number to retrieve. Defaults to 1.
+ page_size (int, optional): The number of agents per page. Defaults to 10.
+
+ Returns:
+ dict: A dictionary containing the list of featured agents, total count, current page number, page size, and total number of pages.
+ """
+ try:
+ # Calculate pagination
+ skip = (page - 1) * page_size
+
+ # Execute the query
+ try:
+ featured_agents = await prisma.models.FeaturedAgent.prisma().find_many(
+ where={
+ "featuredCategories": {"has": category},
+ "isActive": True,
+ "agent": {"is": {"submissionStatus": submission_status}},
+ },
+ include={"agent": {"include": {"AnalyticsTracker": True}}},
+ skip=skip,
+ take=page_size,
+ )
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+
+ # Get total count for pagination info
+ total_count = len(featured_agents)
+
+ return FeaturedAgentResponse(
+ featured_agents=featured_agents,
+ total_count=total_count,
+ page=page,
+ page_size=page_size,
+ total_pages=(total_count + page_size - 1) // page_size,
+ )
+
+ except AgentQueryError as e:
+ # Log the error or handle it as needed
+ raise e from e
+ except ValueError as e:
+ raise AgentQueryError(f"Invalid input parameter: {str(e)}") from e
+ except Exception as e:
+ # Catch any other unexpected exceptions
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}") from e
+
+
+async def remove_featured_category(
+ agent_id: str, category: str
+) -> prisma.models.FeaturedAgent | None:
+ """Adds a featured category to an agent.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ category (str): The category to add to the agent.
+
+ Returns:
+ FeaturedAgentResponse: The updated list of featured agents.
+ """
+ try:
+ # get the existing categories
+ featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
+ where={"agentId": agent_id},
+ include={"agent": True},
+ )
+
+ if not featured_agent:
+ raise AgentQueryError(f"Agent with ID {agent_id} not found.")
+
+ # remove the category from the list
+ featured_agent.featuredCategories.remove(category)
+
+ featured_agent = await prisma.models.FeaturedAgent.prisma().update(
+ where={"agentId": agent_id},
+ data={"featuredCategories": featured_agent.featuredCategories},
+ )
+
+ return featured_agent
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def add_featured_category(
+ agent_id: str, category: str
+) -> prisma.models.FeaturedAgent | None:
+ """Removes a featured category from an agent.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ category (str): The category to remove from the agent.
+
+ Returns:
+ FeaturedAgentResponse: The updated list of featured agents.
+ """
+ try:
+ featured_agent = await prisma.models.FeaturedAgent.prisma().update(
+ where={"agentId": agent_id},
+ data={"featuredCategories": {"push": [category]}},
+ )
+
+ return featured_agent
+
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_agent_featured(agent_id: str) -> prisma.models.FeaturedAgent | None:
+ """Retrieve an agent's featured categories from the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+
+ Returns:
+ FeaturedAgentResponse: The list of featured agents.
+ """
+ try:
+ featured_agent = await prisma.models.FeaturedAgent.prisma().find_unique(
+ where={"agentId": agent_id},
+ )
+ return featured_agent
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_not_featured_agents(
+ page: int = 1, page_size: int = 10
+) -> typing.List[prisma.models.Agents]:
+ """
+ Retrieve a list of not featured agents from the database.
+ """
+ try:
+ agents = await prisma.client.get_client().query_raw(
+ query=f"""
+ SELECT
+ "market"."Agents".id,
+ "market"."Agents"."createdAt",
+ "market"."Agents"."updatedAt",
+ "market"."Agents".version,
+ "market"."Agents".name,
+ LEFT("market"."Agents".description, 500) AS description,
+ "market"."Agents".author,
+ "market"."Agents".keywords,
+ "market"."Agents".categories,
+ "market"."Agents".graph,
+ "market"."Agents"."submissionStatus",
+ "market"."Agents"."submissionDate",
+ "market"."Agents".search::text AS search
+ FROM "market"."Agents"
+ LEFT JOIN "market"."FeaturedAgent" ON "market"."Agents"."id" = "market"."FeaturedAgent"."agentId"
+ WHERE ("market"."FeaturedAgent"."agentId" IS NULL OR "market"."FeaturedAgent"."featuredCategories" = '{{}}')
+ AND "market"."Agents"."submissionStatus" = 'APPROVED'
+ ORDER BY "market"."Agents"."createdAt" DESC
+ LIMIT {page_size} OFFSET {page_size * (page - 1)}
+ """,
+ model=prisma.models.Agents,
+ )
+ return agents
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
+
+
+async def get_all_categories() -> market.model.CategoriesResponse:
+ """
+ Retrieve all unique categories from the database.
+
+ Returns:
+ CategoriesResponse: A list of unique categories.
+ """
+ try:
+ agents = await prisma.models.Agents.prisma().find_many(distinct=["categories"])
+
+ # Aggregate categories on the Python side
+ all_categories = set()
+ for agent in agents:
+ all_categories.update(agent.categories)
+
+ unique_categories = sorted(list(all_categories))
+
+ return market.model.CategoriesResponse(unique_categories=unique_categories)
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception:
+ # Return an empty list of categories in case of unexpected errors
+ return market.model.CategoriesResponse(unique_categories=[])
+
+
+async def create_agent_installed_event(
+ event_data: market.model.AgentInstalledFromMarketplaceEventData,
+):
+ try:
+ await prisma.models.InstallTracker.prisma().create(
+ data={
+ "installedAgentId": event_data.installed_agent_id,
+ "marketplaceAgentId": event_data.marketplace_agent_id,
+ "installationLocation": prisma.enums.InstallationLocation(
+ event_data.installation_location.name
+ ),
+ }
+ )
+ except prisma.errors.PrismaError as e:
+ raise AgentQueryError(f"Database query failed: {str(e)}")
+ except Exception as e:
+ raise AgentQueryError(f"Unexpected error occurred: {str(e)}")
diff --git a/autogpt_platform/market/market/model.py b/autogpt_platform/market/market/model.py
new file mode 100644
index 000000000..14bd017a1
--- /dev/null
+++ b/autogpt_platform/market/market/model.py
@@ -0,0 +1,161 @@
+import datetime
+import typing
+from enum import Enum
+from typing import Generic, Literal, TypeVar, Union
+
+import prisma.enums
+import pydantic
+
+
+class InstallationLocation(str, Enum):
+ LOCAL = "local"
+ CLOUD = "cloud"
+
+
+class AgentInstalledFromMarketplaceEventData(pydantic.BaseModel):
+ marketplace_agent_id: str
+ installed_agent_id: str
+ installation_location: InstallationLocation
+
+
+class AgentInstalledFromTemplateEventData(pydantic.BaseModel):
+ template_id: str
+ installed_agent_id: str
+ installation_location: InstallationLocation
+
+
+class AgentInstalledFromMarketplaceEvent(pydantic.BaseModel):
+ event_name: Literal["agent_installed_from_marketplace"]
+ event_data: AgentInstalledFromMarketplaceEventData
+
+
+class AgentInstalledFromTemplateEvent(pydantic.BaseModel):
+ event_name: Literal["agent_installed_from_template"]
+ event_data: AgentInstalledFromTemplateEventData
+
+
+AnalyticsEvent = Union[
+ AgentInstalledFromMarketplaceEvent, AgentInstalledFromTemplateEvent
+]
+
+
+class AnalyticsRequest(pydantic.BaseModel):
+ event: AnalyticsEvent
+
+
+class AddAgentRequest(pydantic.BaseModel):
+ graph: dict[str, typing.Any]
+ author: str
+ keywords: list[str]
+ categories: list[str]
+
+
+class SubmissionReviewRequest(pydantic.BaseModel):
+ agent_id: str
+ version: int
+ status: prisma.enums.SubmissionStatus
+ comments: str | None
+
+
+class AgentResponse(pydantic.BaseModel):
+ """
+ Represents a response from an agent.
+
+ Attributes:
+ id (str): The ID of the agent.
+ name (str, optional): The name of the agent.
+ description (str, optional): The description of the agent.
+ author (str, optional): The author of the agent.
+ keywords (list[str]): The keywords associated with the agent.
+ categories (list[str]): The categories the agent belongs to.
+ version (int): The version of the agent.
+ createdAt (str): The creation date of the agent.
+ updatedAt (str): The last update date of the agent.
+ """
+
+ id: str
+ name: typing.Optional[str]
+ description: typing.Optional[str]
+ author: typing.Optional[str]
+ keywords: list[str]
+ categories: list[str]
+ version: int
+ createdAt: datetime.datetime
+ updatedAt: datetime.datetime
+ submissionStatus: str
+ views: int = 0
+ downloads: int = 0
+
+
+class AgentDetailResponse(pydantic.BaseModel):
+ """
+ Represents the response data for an agent detail.
+
+ Attributes:
+ id (str): The ID of the agent.
+ name (Optional[str]): The name of the agent.
+ description (Optional[str]): The description of the agent.
+ author (Optional[str]): The author of the agent.
+ keywords (List[str]): The keywords associated with the agent.
+ categories (List[str]): The categories the agent belongs to.
+ version (int): The version of the agent.
+ createdAt (str): The creation date of the agent.
+ updatedAt (str): The last update date of the agent.
+ graph (Dict[str, Any]): The graph data of the agent.
+ """
+
+ id: str
+ name: typing.Optional[str]
+ description: typing.Optional[str]
+ author: typing.Optional[str]
+ keywords: list[str]
+ categories: list[str]
+ version: int
+ createdAt: datetime.datetime
+ updatedAt: datetime.datetime
+ graph: dict[str, typing.Any]
+
+
+class FeaturedAgentResponse(pydantic.BaseModel):
+ """
+ Represents the response data for an agent detail.
+ """
+
+ agentId: str
+ featuredCategories: list[str]
+ createdAt: datetime.datetime
+ updatedAt: datetime.datetime
+ isActive: bool
+
+
+class CategoriesResponse(pydantic.BaseModel):
+ """
+ Represents the response data for a list of categories.
+
+ Attributes:
+ unique_categories (list[str]): The list of unique categories.
+ """
+
+ unique_categories: list[str]
+
+
+T = TypeVar("T")
+
+
+class ListResponse(pydantic.BaseModel, Generic[T]):
+ """
+ Represents a list response.
+
+ Attributes:
+ items (list[T]): The list of items.
+ total_count (int): The total count of items.
+ page (int): The current page number.
+ page_size (int): The number of items per page.
+ total_pages (int): The total number of pages.
+ """
+
+ items: list[T]
+ total_count: int
+ page: int
+ page_size: int
+ total_pages: int
diff --git a/autogpt_platform/market/market/routes/admin.py b/autogpt_platform/market/market/routes/admin.py
new file mode 100644
index 000000000..a3fe65cc9
--- /dev/null
+++ b/autogpt_platform/market/market/routes/admin.py
@@ -0,0 +1,286 @@
+import logging
+import typing
+
+import autogpt_libs.auth
+import fastapi
+import prisma
+import prisma.enums
+import prisma.models
+
+import market.db
+import market.model
+
+logger = logging.getLogger("marketplace")
+
+router = fastapi.APIRouter()
+
+
+@router.delete("/agent/{agent_id}", response_model=market.model.AgentResponse)
+async def delete_agent(
+ agent_id: str,
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+):
+ """
+ Delete an agent and all related records from the database.
+
+ Args:
+ agent_id (str): The ID of the agent to delete.
+
+ Returns:
+ market.model.AgentResponse: The deleted agent's data.
+
+ Raises:
+ fastapi.HTTPException: If the agent is not found or if there's an error during deletion.
+ """
+ try:
+ deleted_agent = await market.db.delete_agent(agent_id)
+ if deleted_agent:
+ return market.model.AgentResponse(**deleted_agent.dict())
+ else:
+ raise fastapi.HTTPException(status_code=404, detail="Agent not found")
+ except market.db.AgentQueryError as e:
+ logger.error(f"Error deleting agent: {e}")
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ logger.error(f"Unexpected error deleting agent: {e}")
+ raise fastapi.HTTPException(
+ status_code=500, detail="An unexpected error occurred"
+ )
+
+
+@router.post("/agent", response_model=market.model.AgentResponse)
+async def create_agent_entry(
+ request: market.model.AddAgentRequest,
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+):
+ """
+ A basic endpoint to create a new agent entry in the database.
+
+ """
+ try:
+ agent = await market.db.create_agent_entry(
+ request.graph["name"],
+ request.graph["description"],
+ request.author,
+ request.keywords,
+ request.categories,
+ prisma.Json(request.graph),
+ )
+
+ return fastapi.responses.PlainTextResponse(agent.model_dump_json())
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/agent/featured/{agent_id}")
+async def set_agent_featured(
+ agent_id: str,
+ categories: list[str] = fastapi.Query(
+ default=["featured"],
+ description="The categories to set the agent as featured in",
+ ),
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> market.model.FeaturedAgentResponse:
+ """
+ A basic endpoint to set an agent as featured in the database.
+ """
+ try:
+ agent = await market.db.set_agent_featured(
+ agent_id, is_active=True, featured_categories=categories
+ )
+ return market.model.FeaturedAgentResponse(**agent.model_dump())
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/agent/featured/{agent_id}")
+async def get_agent_featured(
+ agent_id: str,
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> market.model.FeaturedAgentResponse | None:
+ """
+ A basic endpoint to get an agent as featured in the database.
+ """
+ try:
+ agent = await market.db.get_agent_featured(agent_id)
+ if agent:
+ return market.model.FeaturedAgentResponse(**agent.model_dump())
+ else:
+ return None
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.delete("/agent/featured/{agent_id}")
+async def unset_agent_featured(
+ agent_id: str,
+ category: str = "featured",
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> market.model.FeaturedAgentResponse | None:
+ """
+ A basic endpoint to unset an agent as featured in the database.
+ """
+ try:
+ featured = await market.db.remove_featured_category(agent_id, category=category)
+ if featured:
+ return market.model.FeaturedAgentResponse(**featured.model_dump())
+ else:
+ return None
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/agent/not-featured")
+async def get_not_featured_agents(
+ page: int = fastapi.Query(1, ge=1, description="Page number"),
+ page_size: int = fastapi.Query(
+ 10, ge=1, le=100, description="Number of items per page"
+ ),
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> market.model.ListResponse[market.model.AgentResponse]:
+ """
+ A basic endpoint to get all not featured agents in the database.
+ """
+ try:
+ agents = await market.db.get_not_featured_agents(page=page, page_size=page_size)
+ return market.model.ListResponse(
+ items=[
+ market.model.AgentResponse(**agent.model_dump()) for agent in agents
+ ],
+ total_count=len(agents),
+ page=page,
+ page_size=page_size,
+ total_pages=999,
+ )
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.get(
+ "/agent/submissions",
+ response_model=market.model.ListResponse[market.model.AgentResponse],
+)
+async def get_agent_submissions(
+ page: int = fastapi.Query(1, ge=1, description="Page number"),
+ page_size: int = fastapi.Query(
+ 10, ge=1, le=100, description="Number of items per page"
+ ),
+ name: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by agent name"
+ ),
+ keyword: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by keyword"
+ ),
+ category: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by category"
+ ),
+ description: typing.Optional[str] = fastapi.Query(
+ None, description="Fuzzy search in description"
+ ),
+ description_threshold: int = fastapi.Query(
+ 60, ge=0, le=100, description="Fuzzy search threshold"
+ ),
+ sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
+ sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
+ "desc", description="Sort order (asc or desc)"
+ ),
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> market.model.ListResponse[market.model.AgentResponse]:
+ logger.info("Getting agent submissions")
+ try:
+ result = await market.db.get_agents(
+ page=page,
+ page_size=page_size,
+ name=name,
+ keyword=keyword,
+ category=category,
+ description=description,
+ description_threshold=description_threshold,
+ sort_by=sort_by,
+ sort_order=sort_order,
+ submission_status=prisma.enums.SubmissionStatus.PENDING,
+ )
+
+ agents = [
+ market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
+ ]
+
+ return market.model.ListResponse(
+ items=agents,
+ total_count=result["total_count"],
+ page=result["page"],
+ page_size=result["page_size"],
+ total_pages=result["total_pages"],
+ )
+
+ except market.db.AgentQueryError as e:
+ logger.error(f"Error getting agent submissions: {e}")
+ raise fastapi.HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ logger.error(f"Error getting agent submissions: {e}")
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {e}"
+ )
+
+
+@router.post("/agent/submissions")
+async def review_submission(
+ review_request: market.model.SubmissionReviewRequest,
+ user: autogpt_libs.auth.User = fastapi.Depends(
+ autogpt_libs.auth.requires_admin_user
+ ),
+) -> prisma.models.Agents | None:
+ """
+ A basic endpoint to review a submission in the database.
+ """
+ logger.info(
+ f"Reviewing submission: {review_request.agent_id}, {review_request.version}"
+ )
+ try:
+ agent = await market.db.update_agent_entry(
+ agent_id=review_request.agent_id,
+ version=review_request.version,
+ submission_state=review_request.status,
+ comments=review_request.comments,
+ )
+ return agent
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+
+
+@router.get("/categories")
+async def get_categories() -> market.model.CategoriesResponse:
+ """
+ A basic endpoint to get all available categories.
+ """
+ try:
+ categories = await market.db.get_all_categories()
+ return categories
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
diff --git a/autogpt_platform/market/market/routes/admin_tests.py b/autogpt_platform/market/market/routes/admin_tests.py
new file mode 100644
index 000000000..c305e8c83
--- /dev/null
+++ b/autogpt_platform/market/market/routes/admin_tests.py
@@ -0,0 +1,76 @@
+import datetime
+from unittest import mock
+
+import autogpt_libs.auth.middleware
+import fastapi
+import fastapi.testclient
+import prisma.enums
+import prisma.models
+
+import market.app
+
+client = fastapi.testclient.TestClient(market.app.app)
+
+
+async def override_auth_middleware(request: fastapi.Request):
+ return {"sub": "3e53486c-cf57-477e-ba2a-cb02dc828e1a", "role": "admin"}
+
+
+market.app.app.dependency_overrides[autogpt_libs.auth.middleware.auth_middleware] = (
+ override_auth_middleware
+)
+
+
+def test_get_submissions():
+ with mock.patch("market.db.get_agents") as mock_get_agents:
+ mock_get_agents.return_value = {
+ "agents": [],
+ "total_count": 0,
+ "page": 1,
+ "page_size": 10,
+ "total_pages": 0,
+ }
+ response = client.get(
+ "/api/v1/market/admin/agent/submissions?page=1&page_size=10&description_threshold=60&sort_by=createdAt&sort_order=desc",
+ headers={"Bearer": ""},
+ )
+ assert response.status_code == 200
+ assert response.json() == {
+ "agents": [],
+ "total_count": 0,
+ "page": 1,
+ "page_size": 10,
+ "total_pages": 0,
+ }
+
+
+def test_review_submission():
+ with mock.patch("market.db.update_agent_entry") as mock_update_agent_entry:
+ mock_update_agent_entry.return_value = prisma.models.Agents(
+ id="aaa-bbb-ccc",
+ version=1,
+ createdAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
+ updatedAt=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
+ submissionStatus=prisma.enums.SubmissionStatus.APPROVED,
+ submissionDate=datetime.datetime.fromisoformat("2021-10-01T00:00:00+00:00"),
+ submissionReviewComments="Looks good",
+ submissionReviewDate=datetime.datetime.fromisoformat(
+ "2021-10-01T00:00:00+00:00"
+ ),
+ keywords=["test"],
+ categories=["test"],
+ graph='{"name": "test", "description": "test"}', # type: ignore
+ )
+ response = client.post(
+ "/api/v1/market/admin/agent/submissions",
+ headers={
+ "Authorization": "Bearer token"
+ }, # Assuming you need an authorization token
+ json={
+ "agent_id": "aaa-bbb-ccc",
+ "version": 1,
+ "status": "APPROVED",
+ "comments": "Looks good",
+ },
+ )
+ assert response.status_code == 200
diff --git a/autogpt_platform/market/market/routes/agents.py b/autogpt_platform/market/market/routes/agents.py
new file mode 100644
index 000000000..672dfb64e
--- /dev/null
+++ b/autogpt_platform/market/market/routes/agents.py
@@ -0,0 +1,368 @@
+import json
+import tempfile
+import typing
+
+import fastapi
+import fastapi.responses
+import prisma
+import prisma.enums
+
+import market.db
+import market.model
+import market.utils.analytics
+
+router = fastapi.APIRouter()
+
+
+@router.get(
+ "/agents", response_model=market.model.ListResponse[market.model.AgentResponse]
+)
+async def list_agents(
+ page: int = fastapi.Query(1, ge=1, description="Page number"),
+ page_size: int = fastapi.Query(
+ 10, ge=1, le=100, description="Number of items per page"
+ ),
+ name: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by agent name"
+ ),
+ keyword: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by keyword"
+ ),
+ category: typing.Optional[str] = fastapi.Query(
+ None, description="Filter by category"
+ ),
+ description: typing.Optional[str] = fastapi.Query(
+ None, description="Fuzzy search in description"
+ ),
+ description_threshold: int = fastapi.Query(
+ 60, ge=0, le=100, description="Fuzzy search threshold"
+ ),
+ sort_by: str = fastapi.Query("createdAt", description="Field to sort by"),
+ sort_order: typing.Literal["asc", "desc"] = fastapi.Query(
+ "desc", description="Sort order (asc or desc)"
+ ),
+ submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
+ default=prisma.enums.SubmissionStatus.APPROVED,
+ description="Filter by submission status",
+ ),
+):
+ """
+ Retrieve a list of agents based on the provided filters.
+
+ Args:
+ page (int): Page number (default: 1).
+ page_size (int): Number of items per page (default: 10, min: 1, max: 100).
+ name (str, optional): Filter by agent name.
+ keyword (str, optional): Filter by keyword.
+ category (str, optional): Filter by category.
+ description (str, optional): Fuzzy search in description.
+ description_threshold (int): Fuzzy search threshold (default: 60, min: 0, max: 100).
+ sort_by (str): Field to sort by (default: "createdAt").
+ sort_order (str): Sort order (asc or desc) (default: "desc").
+ submission_status (str): Filter by submission status (default: "APPROVED").
+
+ Returns:
+ market.model.ListResponse[market.model.AgentResponse]: A response containing the list of agents and pagination information.
+
+ Raises:
+ HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
+ """
+ try:
+ result = await market.db.get_agents(
+ page=page,
+ page_size=page_size,
+ name=name,
+ keyword=keyword,
+ category=category,
+ description=description,
+ description_threshold=description_threshold,
+ sort_by=sort_by,
+ sort_order=sort_order,
+ submission_status=submission_status,
+ )
+
+ agents = [
+ market.model.AgentResponse(**agent.dict()) for agent in result["agents"]
+ ]
+
+ return market.model.ListResponse(
+ items=agents,
+ total_count=result["total_count"],
+ page=result["page"],
+ page_size=result["page_size"],
+ total_pages=result["total_pages"],
+ )
+
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {e}"
+ )
+
+
+@router.get("/agents/{agent_id}", response_model=market.model.AgentDetailResponse)
+async def get_agent_details_endpoint(
+ background_tasks: fastapi.BackgroundTasks,
+ agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
+ version: typing.Optional[int] = fastapi.Query(
+ None, description="Specific version of the agent"
+ ),
+):
+ """
+ Retrieve details of a specific agent.
+
+ Args:
+ agent_id (str): The ID of the agent to retrieve.
+ version (Optional[int]): Specific version of the agent (default: None).
+
+ Returns:
+ market.model.AgentDetailResponse: The response containing the agent details.
+
+ Raises:
+ HTTPException: If the agent is not found or an unexpected error occurs.
+ """
+ try:
+ agent = await market.db.get_agent_details(agent_id, version)
+ background_tasks.add_task(market.utils.analytics.track_view, agent_id)
+ return market.model.AgentDetailResponse(**agent.model_dump())
+
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=404, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {str(e)}"
+ )
+
+
+@router.get("/agents/{agent_id}/download")
+async def download_agent(
+ background_tasks: fastapi.BackgroundTasks,
+ agent_id: str = fastapi.Path(..., description="The ID of the agent to retrieve"),
+ version: typing.Optional[int] = fastapi.Query(
+ None, description="Specific version of the agent"
+ ),
+):
+ """
+ Download details of a specific agent.
+
+ NOTE: This is the same as agent details, however it also triggers
+ the "download" tracking. We don't actually want to download a file though
+
+ Args:
+ agent_id (str): The ID of the agent to retrieve.
+ version (Optional[int]): Specific version of the agent (default: None).
+
+ Returns:
+ market.model.AgentDetailResponse: The response containing the agent details.
+
+ Raises:
+ HTTPException: If the agent is not found or an unexpected error occurs.
+ """
+ try:
+ agent = await market.db.get_agent_details(agent_id, version)
+ background_tasks.add_task(market.utils.analytics.track_download, agent_id)
+ return market.model.AgentDetailResponse(**agent.model_dump())
+
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=404, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {str(e)}"
+ )
+
+
+@router.get("/agents/{agent_id}/download-file")
+async def download_agent_file(
+ background_tasks: fastapi.BackgroundTasks,
+ agent_id: str = fastapi.Path(..., description="The ID of the agent to download"),
+ version: typing.Optional[int] = fastapi.Query(
+ None, description="Specific version of the agent"
+ ),
+) -> fastapi.responses.FileResponse:
+ """
+ Download the agent file by streaming its content.
+
+ Args:
+ agent_id (str): The ID of the agent to download.
+ version (Optional[int]): Specific version of the agent to download.
+
+ Returns:
+ StreamingResponse: A streaming response containing the agent's graph data.
+
+ Raises:
+ HTTPException: If the agent is not found or an unexpected error occurs.
+ """
+ agent = await market.db.get_agent_details(agent_id, version)
+
+ graph_data: prisma.Json = agent.graph
+
+ background_tasks.add_task(market.utils.analytics.track_download, agent_id)
+
+ file_name = f"agent_{agent_id}_v{version or 'latest'}.json"
+
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".json", delete=False
+ ) as tmp_file:
+ tmp_file.write(json.dumps(graph_data))
+ tmp_file.flush()
+
+ return fastapi.responses.FileResponse(
+ tmp_file.name, filename=file_name, media_type="application/json"
+ )
+
+
+# top agents by downloads
+@router.get(
+ "/top-downloads/agents",
+ response_model=market.model.ListResponse[market.model.AgentResponse],
+)
+async def top_agents_by_downloads(
+ page: int = fastapi.Query(1, ge=1, description="Page number"),
+ page_size: int = fastapi.Query(
+ 10, ge=1, le=100, description="Number of items per page"
+ ),
+ submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
+ default=prisma.enums.SubmissionStatus.APPROVED,
+ description="Filter by submission status",
+ ),
+) -> market.model.ListResponse[market.model.AgentResponse]:
+ """
+ Retrieve a list of top agents based on the number of downloads.
+
+ Args:
+ page (int): Page number (default: 1).
+ page_size (int): Number of items per page (default: 10, min: 1, max: 100).
+ submission_status (str): Filter by submission status (default: "APPROVED").
+
+ Returns:
+ market.model.ListResponse[market.model.AgentResponse]: A response containing the list of top agents and pagination information.
+
+ Raises:
+ HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
+ """
+ try:
+ result = await market.db.get_top_agents_by_downloads(
+ page=page,
+ page_size=page_size,
+ submission_status=submission_status,
+ )
+
+ ret = market.model.ListResponse(
+ total_count=result.total_count,
+ page=result.page,
+ page_size=result.page_size,
+ total_pages=result.total_pages,
+ items=[
+ market.model.AgentResponse(
+ id=item.agent.id,
+ name=item.agent.name,
+ description=item.agent.description,
+ author=item.agent.author,
+ keywords=item.agent.keywords,
+ categories=item.agent.categories,
+ version=item.agent.version,
+ createdAt=item.agent.createdAt,
+ updatedAt=item.agent.updatedAt,
+ views=item.views,
+ downloads=item.downloads,
+ submissionStatus=item.agent.submissionStatus,
+ )
+ for item in result.items
+ if item.agent is not None
+ ],
+ )
+
+ return ret
+
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {e}"
+ ) from e
+
+
+@router.get(
+ "/featured/agents",
+ response_model=market.model.ListResponse[market.model.AgentResponse],
+)
+async def get_featured_agents(
+ category: str = fastapi.Query(
+ "featured", description="Category of featured agents"
+ ),
+ page: int = fastapi.Query(1, ge=1, description="Page number"),
+ page_size: int = fastapi.Query(
+ 10, ge=1, le=100, description="Number of items per page"
+ ),
+ submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
+ default=prisma.enums.SubmissionStatus.APPROVED,
+ description="Filter by submission status",
+ ),
+):
+ """
+ Retrieve a list of featured agents based on the provided category.
+
+ Args:
+ category (str): Category of featured agents (default: "featured").
+ page (int): Page number (default: 1).
+ page_size (int): Number of items per page (default: 10, min: 1, max: 100).
+ submission_status (str): Filter by submission status (default: "APPROVED").
+
+ Returns:
+ market.model.ListResponse[market.model.AgentResponse]: A response containing the list of featured agents and pagination information.
+
+ Raises:
+ HTTPException: If there is a client error (status code 400) or an unexpected error (status code 500).
+ """
+ try:
+ result = await market.db.get_featured_agents(
+ category=category,
+ page=page,
+ page_size=page_size,
+ submission_status=submission_status,
+ )
+
+ ret = market.model.ListResponse(
+ total_count=result.total_count,
+ page=result.page,
+ page_size=result.page_size,
+ total_pages=result.total_pages,
+ items=[
+ market.model.AgentResponse(
+ id=item.agent.id,
+ name=item.agent.name,
+ description=item.agent.description,
+ author=item.agent.author,
+ keywords=item.agent.keywords,
+ categories=item.agent.categories,
+ version=item.agent.version,
+ createdAt=item.agent.createdAt,
+ updatedAt=item.agent.updatedAt,
+ views=(
+ item.agent.AnalyticsTracker[0].views
+ if item.agent.AnalyticsTracker
+ and len(item.agent.AnalyticsTracker) > 0
+ else 0
+ ),
+ downloads=(
+ item.agent.AnalyticsTracker[0].downloads
+ if item.agent.AnalyticsTracker
+ and len(item.agent.AnalyticsTracker) > 0
+ else 0
+ ),
+ submissionStatus=item.agent.submissionStatus,
+ )
+ for item in result.featured_agents
+ if item.agent is not None
+ ],
+ )
+
+ return ret
+
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=400, detail=str(e)) from e
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {e}"
+ ) from e
diff --git a/autogpt_platform/market/market/routes/analytics.py b/autogpt_platform/market/market/routes/analytics.py
new file mode 100644
index 000000000..87adbca07
--- /dev/null
+++ b/autogpt_platform/market/market/routes/analytics.py
@@ -0,0 +1,26 @@
+import fastapi
+
+import market.db
+import market.model
+
+router = fastapi.APIRouter()
+
+
+@router.post("/agent-installed")
+async def agent_installed_endpoint(
+ event_data: market.model.AgentInstalledFromMarketplaceEventData,
+):
+ """
+ Endpoint to track agent installation events from the marketplace.
+
+ Args:
+ event_data (market.model.AgentInstalledFromMarketplaceEventData): The event data.
+ """
+ try:
+ await market.db.create_agent_installed_event(event_data)
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(
+ status_code=500, detail=f"An unexpected error occurred: {e}"
+ )
diff --git a/autogpt_platform/market/market/routes/search.py b/autogpt_platform/market/market/routes/search.py
new file mode 100644
index 000000000..15ef3ffa3
--- /dev/null
+++ b/autogpt_platform/market/market/routes/search.py
@@ -0,0 +1,56 @@
+import typing
+
+import fastapi
+import prisma.enums
+
+import market.db
+import market.model
+import market.utils.extension_types
+
+router = fastapi.APIRouter()
+
+
+@router.get("/search")
+async def search(
+ query: str,
+ page: int = fastapi.Query(1, description="The pagination page to start on"),
+ page_size: int = fastapi.Query(
+ 10, description="The number of items to return per page"
+ ),
+ categories: typing.List[str] = fastapi.Query(
+ None, description="The categories to filter by"
+ ),
+ description_threshold: int = fastapi.Query(
+ 60, description="The number of characters to return from the description"
+ ),
+ sort_by: str = fastapi.Query("rank", description="Sorting by column"),
+ sort_order: typing.Literal["desc", "asc"] = fastapi.Query(
+ "desc", description="The sort order based on sort_by"
+ ),
+ submission_status: prisma.enums.SubmissionStatus = fastapi.Query(
+ prisma.enums.SubmissionStatus.APPROVED,
+ description="The submission status to filter by",
+ ),
+) -> market.model.ListResponse[market.utils.extension_types.AgentsWithRank]:
+ """searches endpoint for agents
+
+ Args:
+ query (str): the search query
+ page (int, optional): the pagination page to start on. Defaults to 1.
+ page_size (int, optional): the number of items to return per page. Defaults to 10.
+ category (str | None, optional): the agent category to filter by. None is no filter. Defaults to None.
+ description_threshold (int, optional): the number of characters to return from the description. Defaults to 60.
+ sort_by (str, optional): Sorting by column. Defaults to "rank".
+ sort_order ('asc' | 'desc', optional): the sort order based on sort_by. Defaults to "desc".
+ """
+ agents = await market.db.search_db(
+ query=query,
+ page=page,
+ page_size=page_size,
+ categories=categories,
+ description_threshold=description_threshold,
+ sort_by=sort_by,
+ sort_order=sort_order,
+ submission_status=submission_status,
+ )
+ return agents
diff --git a/autogpt_platform/market/market/routes/submissions.py b/autogpt_platform/market/market/routes/submissions.py
new file mode 100644
index 000000000..49a4dea14
--- /dev/null
+++ b/autogpt_platform/market/market/routes/submissions.py
@@ -0,0 +1,35 @@
+import autogpt_libs.auth
+import fastapi
+import fastapi.responses
+import prisma
+
+import market.db
+import market.model
+import market.utils.analytics
+
+router = fastapi.APIRouter()
+
+
+@router.post("/agents/submit", response_model=market.model.AgentResponse)
+async def submit_agent(
+ request: market.model.AddAgentRequest,
+ user: autogpt_libs.auth.User = fastapi.Depends(autogpt_libs.auth.requires_user),
+):
+ """
+ A basic endpoint to create a new agent entry in the database.
+ """
+ try:
+ agent = await market.db.create_agent_entry(
+ request.graph["name"],
+ request.graph["description"],
+ request.author,
+ request.keywords,
+ request.categories,
+ prisma.Json(request.graph),
+ )
+
+ return fastapi.responses.PlainTextResponse(agent.model_dump_json())
+ except market.db.AgentQueryError as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
+ except Exception as e:
+ raise fastapi.HTTPException(status_code=500, detail=str(e))
diff --git a/autogpt_platform/market/market/utils/analytics.py b/autogpt_platform/market/market/utils/analytics.py
new file mode 100644
index 000000000..71dcaf078
--- /dev/null
+++ b/autogpt_platform/market/market/utils/analytics.py
@@ -0,0 +1,47 @@
+import prisma.models
+
+
+async def track_download(agent_id: str):
+ """
+ Track the download event in the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ version (int | None, optional): The version of the agent. Defaults to None.
+
+ Raises:
+ Exception: If there is an error tracking the download event.
+ """
+ try:
+ await prisma.models.AnalyticsTracker.prisma().upsert(
+ where={"agentId": agent_id},
+ data={
+ "update": {"downloads": {"increment": 1}},
+ "create": {"agentId": agent_id, "downloads": 1, "views": 0},
+ },
+ )
+ except Exception as e:
+ raise Exception(f"Error tracking download event: {str(e)}")
+
+
+async def track_view(agent_id: str):
+ """
+ Track the view event in the database.
+
+ Args:
+ agent_id (str): The ID of the agent.
+ version (int | None, optional): The version of the agent. Defaults to None.
+
+ Raises:
+ Exception: If there is an error tracking the view event.
+ """
+ try:
+ await prisma.models.AnalyticsTracker.prisma().upsert(
+ where={"agentId": agent_id},
+ data={
+ "update": {"views": {"increment": 1}},
+ "create": {"agentId": agent_id, "downloads": 0, "views": 1},
+ },
+ )
+ except Exception as e:
+ raise Exception(f"Error tracking view event: {str(e)}")
diff --git a/autogpt_platform/market/market/utils/extension_types.py b/autogpt_platform/market/market/utils/extension_types.py
new file mode 100644
index 000000000..d76bbb19f
--- /dev/null
+++ b/autogpt_platform/market/market/utils/extension_types.py
@@ -0,0 +1,5 @@
+import prisma.models
+
+
+class AgentsWithRank(prisma.models.Agents):
+ rank: float
diff --git a/autogpt_platform/market/market/utils/partial_types.py b/autogpt_platform/market/market/utils/partial_types.py
new file mode 100644
index 000000000..3ec3bdc2b
--- /dev/null
+++ b/autogpt_platform/market/market/utils/partial_types.py
@@ -0,0 +1,6 @@
+import prisma.models
+
+prisma.models.Agents.create_partial(
+ "AgentOnlyDescriptionNameAuthorIdCategories",
+ include={"name", "author", "id", "categories"},
+)
diff --git a/autogpt_platform/market/migrations/20240731181721_base_migration/migration.sql b/autogpt_platform/market/migrations/20240731181721_base_migration/migration.sql
new file mode 100644
index 000000000..57bdb6d04
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240731181721_base_migration/migration.sql
@@ -0,0 +1,61 @@
+-- CreateTable
+CREATE TABLE "Agents" (
+ "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "version" INTEGER NOT NULL DEFAULT 1,
+ "name" TEXT,
+ "description" TEXT,
+ "author" TEXT,
+ "keywords" TEXT[],
+ "categories" TEXT[],
+ "search" tsvector DEFAULT ''::tsvector,
+ "graph" JSONB NOT NULL,
+
+ CONSTRAINT "Agents_pkey" PRIMARY KEY ("id","version")
+);
+
+-- CreateTable
+CREATE TABLE "AnalyticsTracker" (
+ "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ "agentId" UUID NOT NULL,
+ "views" INTEGER NOT NULL,
+ "downloads" INTEGER NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Agents_id_key" ON "Agents"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AnalyticsTracker_id_key" ON "AnalyticsTracker"("id");
+
+-- AddForeignKey
+ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+
+-- Add trigger to update the search column with the tsvector of the agent
+-- Function to be invoked by trigger
+
+CREATE OR REPLACE FUNCTION update_tsvector_column() RETURNS TRIGGER AS $$
+
+BEGIN
+
+NEW.search := to_tsvector('english', COALESCE(NEW.description, '')|| ' ' ||COALESCE(NEW.name, '')|| ' ' ||COALESCE(NEW.author, ''));
+
+RETURN NEW;
+
+END;
+
+$$ LANGUAGE plpgsql SECURITY definer SET search_path = public, pg_temp;
+
+-- Trigger that keeps the TSVECTOR up to date
+
+DROP TRIGGER IF EXISTS "update_tsvector" ON "Agents";
+
+CREATE TRIGGER "update_tsvector"
+
+BEFORE INSERT OR UPDATE ON "Agents"
+
+FOR EACH ROW
+
+EXECUTE FUNCTION update_tsvector_column ();
\ No newline at end of file
diff --git a/autogpt_platform/market/migrations/20240731213728_unique_agent_id/migration.sql b/autogpt_platform/market/migrations/20240731213728_unique_agent_id/migration.sql
new file mode 100644
index 000000000..fd421f6a6
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240731213728_unique_agent_id/migration.sql
@@ -0,0 +1,11 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_pkey" PRIMARY KEY ("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId");
diff --git a/autogpt_platform/market/migrations/20240802100955_add_featured_agents/migration.sql b/autogpt_platform/market/migrations/20240802100955_add_featured_agents/migration.sql
new file mode 100644
index 000000000..e615e8671
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240802100955_add_featured_agents/migration.sql
@@ -0,0 +1,20 @@
+-- CreateTable
+CREATE TABLE "FeaturedAgent" (
+ "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ "agentId" UUID NOT NULL,
+ "is_featured" BOOLEAN NOT NULL,
+ "category" TEXT NOT NULL DEFAULT 'featured',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "FeaturedAgent_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "FeaturedAgent_id_key" ON "FeaturedAgent"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "FeaturedAgent_agentId_key" ON "FeaturedAgent"("agentId");
+
+-- AddForeignKey
+ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/autogpt_platform/market/migrations/20240802174953_default_is_featured_to_false/migration.sql b/autogpt_platform/market/migrations/20240802174953_default_is_featured_to_false/migration.sql
new file mode 100644
index 000000000..0893c3785
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240802174953_default_is_featured_to_false/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "FeaturedAgent" ALTER COLUMN "is_featured" SET DEFAULT false;
diff --git a/autogpt_platform/market/migrations/20240808080208_added_submissions/migration.sql b/autogpt_platform/market/migrations/20240808080208_added_submissions/migration.sql
new file mode 100644
index 000000000..e30f090fb
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240808080208_added_submissions/migration.sql
@@ -0,0 +1,8 @@
+-- CreateEnum
+CREATE TYPE "SubmissionStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
+
+-- AlterTable
+ALTER TABLE "Agents" ADD COLUMN "submissionDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ADD COLUMN "submissionReviewComments" TEXT,
+ADD COLUMN "submissionReviewDate" TIMESTAMP(3),
+ADD COLUMN "submissionStatus" "SubmissionStatus" NOT NULL DEFAULT 'PENDING';
diff --git a/autogpt_platform/market/migrations/20240829002156_make_featured_category_a_list_of_categories_and_is_featured_is_active/migration.sql b/autogpt_platform/market/migrations/20240829002156_make_featured_category_a_list_of_categories_and_is_featured_is_active/migration.sql
new file mode 100644
index 000000000..3dc0ca322
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240829002156_make_featured_category_a_list_of_categories_and_is_featured_is_active/migration.sql
@@ -0,0 +1,12 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `category` on the `FeaturedAgent` table. All the data in the column will be lost.
+ - You are about to drop the column `is_featured` on the `FeaturedAgent` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "FeaturedAgent" DROP COLUMN "category",
+DROP COLUMN "is_featured",
+ADD COLUMN "featuredCategories" TEXT[],
+ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT false;
diff --git a/autogpt_platform/market/migrations/20240905162237_/migration.sql b/autogpt_platform/market/migrations/20240905162237_/migration.sql
new file mode 100644
index 000000000..ca2dc97bc
--- /dev/null
+++ b/autogpt_platform/market/migrations/20240905162237_/migration.sql
@@ -0,0 +1,19 @@
+-- CreateEnum
+CREATE TYPE "InstallationLocation" AS ENUM ('LOCAL', 'CLOUD');
+
+-- CreateTable
+CREATE TABLE "InstallTracker" (
+ "id" UUID NOT NULL DEFAULT gen_random_uuid(),
+ "marketplaceAgentId" UUID NOT NULL,
+ "installedAgentId" UUID NOT NULL,
+ "installationLocation" "InstallationLocation" NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "InstallTracker_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "InstallTracker_marketplaceAgentId_installedAgentId_key" ON "InstallTracker"("marketplaceAgentId", "installedAgentId");
+
+-- AddForeignKey
+ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/autogpt_platform/market/migrations/20241003134209_update_foreign_key_on_delete/migration.sql b/autogpt_platform/market/migrations/20241003134209_update_foreign_key_on_delete/migration.sql
new file mode 100644
index 000000000..2e1bdc7b7
--- /dev/null
+++ b/autogpt_platform/market/migrations/20241003134209_update_foreign_key_on_delete/migration.sql
@@ -0,0 +1,20 @@
+-- DropForeignKey
+ALTER TABLE "AnalyticsTracker" DROP CONSTRAINT "AnalyticsTracker_agentId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "FeaturedAgent" DROP CONSTRAINT "FeaturedAgent_agentId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "InstallTracker" DROP CONSTRAINT "InstallTracker_marketplaceAgentId_fkey";
+
+-- DropIndex
+DROP INDEX "AnalyticsTracker_agentId_key";
+
+-- AddForeignKey
+ALTER TABLE "AnalyticsTracker" ADD CONSTRAINT "AnalyticsTracker_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InstallTracker" ADD CONSTRAINT "InstallTracker_marketplaceAgentId_fkey" FOREIGN KEY ("marketplaceAgentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "FeaturedAgent" ADD CONSTRAINT "FeaturedAgent_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agents"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql b/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql
new file mode 100644
index 000000000..c1ce4b5de
--- /dev/null
+++ b/autogpt_platform/market/migrations/20241014173713_add_unique_restriction/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[agentId]` on the table `AnalyticsTracker` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- CreateIndex
+CREATE UNIQUE INDEX "AnalyticsTracker_agentId_key" ON "AnalyticsTracker"("agentId");
diff --git a/autogpt_platform/market/migrations/migration_lock.toml b/autogpt_platform/market/migrations/migration_lock.toml
new file mode 100644
index 000000000..fbffa92c2
--- /dev/null
+++ b/autogpt_platform/market/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/autogpt_platform/market/poetry.lock b/autogpt_platform/market/poetry.lock
new file mode 100644
index 000000000..f110bd3fb
--- /dev/null
+++ b/autogpt_platform/market/poetry.lock
@@ -0,0 +1,1301 @@
+# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
+[[package]]
+name = "anyio"
+version = "4.4.0"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
+ {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+]
+
+[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 = "autogpt-libs"
+version = "0.1.0"
+description = "Shared libraries across NextGen AutoGPT"
+optional = false
+python-versions = ">=3.10,<4.0"
+files = []
+develop = false
+
+[package.dependencies]
+pyjwt = "^2.8.0"
+python-dotenv = "^1.0.1"
+
+[package.source]
+type = "directory"
+url = "../autogpt_libs"
+
+[[package]]
+name = "black"
+version = "24.10.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
+ {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
+ {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
+ {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
+ {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
+ {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
+ {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
+ {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
+ {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
+ {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
+ {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
+ {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
+ {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
+ {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
+ {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
+ {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
+ {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
+ {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
+ {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
+ {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
+ {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
+ {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.10)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "certifi"
+version = "2024.7.4"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
+ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[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 = "exceptiongroup"
+version = "1.2.2"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+ {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "fastapi"
+version = "0.115.6"
+description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"},
+ {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"},
+]
+
+[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.40.0,<0.42.0"
+typing-extensions = ">=4.8.0"
+
+[package.extras]
+all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "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)"]
+standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
+
+[[package]]
+name = "fuzzywuzzy"
+version = "0.18.0"
+description = "Fuzzy string matching in python"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fuzzywuzzy-0.18.0-py2.py3-none-any.whl", hash = "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993"},
+ {file = "fuzzywuzzy-0.18.0.tar.gz", hash = "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8"},
+]
+
+[package.extras]
+speedup = ["python-levenshtein (>=0.12)"]
+
+[[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 = "httpcore"
+version = "1.0.5"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
+ {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
+]
+
+[package.dependencies]
+certifi = "*"
+h11 = ">=0.13,<0.15"
+
+[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<0.26.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.27.0"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
+ {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
+]
+
+[package.dependencies]
+anyio = "*"
+certifi = "*"
+httpcore = "==1.*"
+idna = "*"
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotli", "brotlicffi"]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+
+[[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 = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "isort"
+version = "5.13.2"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "jinja2"
+version = "3.1.4"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+ {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "levenshtein"
+version = "0.26.1"
+description = "Python extension for computing string edit distances and similarities."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "levenshtein-0.26.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8dc4a4aecad538d944a1264c12769c99e3c0bf8e741fc5e454cc954913befb2e"},
+ {file = "levenshtein-0.26.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec108f368c12b25787c8b1a4537a1452bc53861c3ee4abc810cc74098278edcd"},
+ {file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69229d651c97ed5b55b7ce92481ed00635cdbb80fbfb282a22636e6945dc52d5"},
+ {file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79dcd157046d62482a7719b08ba9e3ce9ed3fc5b015af8ea989c734c702aedd4"},
+ {file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f53f9173ae21b650b4ed8aef1d0ad0c37821f367c221a982f4d2922b3044e0d"},
+ {file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3956f3c5c229257dbeabe0b6aacd2c083ebcc1e335842a6ff2217fe6cc03b6b"},
+ {file = "levenshtein-0.26.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1e83af732726987d2c4cd736f415dae8b966ba17b7a2239c8b7ffe70bfb5543"},
+ {file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4f052c55046c2a9c9b5f742f39e02fa6e8db8039048b8c1c9e9fdd27c8a240a1"},
+ {file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9895b3a98f6709e293615fde0dcd1bb0982364278fa2072361a1a31b3e388b7a"},
+ {file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a3777de1d8bfca054465229beed23994f926311ce666f5a392c8859bb2722f16"},
+ {file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:81c57e1135c38c5e6e3675b5e2077d8a8d3be32bf0a46c57276c092b1dffc697"},
+ {file = "levenshtein-0.26.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:91d5e7d984891df3eff7ea9fec8cf06fdfacc03cd074fd1a410435706f73b079"},
+ {file = "levenshtein-0.26.1-cp310-cp310-win32.whl", hash = "sha256:f48abff54054b4142ad03b323e80aa89b1d15cabc48ff49eb7a6ff7621829a56"},
+ {file = "levenshtein-0.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:79dd6ad799784ea7b23edd56e3bf94b3ca866c4c6dee845658ee75bb4aefdabf"},
+ {file = "levenshtein-0.26.1-cp310-cp310-win_arm64.whl", hash = "sha256:3351ddb105ef010cc2ce474894c5d213c83dddb7abb96400beaa4926b0b745bd"},
+ {file = "levenshtein-0.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44c51f5d33b3cfb9db518b36f1288437a509edd82da94c4400f6a681758e0cb6"},
+ {file = "levenshtein-0.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56b93203e725f9df660e2afe3d26ba07d71871b6d6e05b8b767e688e23dfb076"},
+ {file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:270d36c5da04a0d89990660aea8542227cbd8f5bc34e9fdfadd34916ff904520"},
+ {file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:480674c05077eeb0b0f748546d4fcbb386d7c737f9fff0010400da3e8b552942"},
+ {file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13946e37323728695ba7a22f3345c2e907d23f4600bc700bf9b4352fb0c72a48"},
+ {file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceb673f572d1d0dc9b1cd75792bb8bad2ae8eb78a7c6721e23a3867d318cb6f2"},
+ {file = "levenshtein-0.26.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42d6fa242e3b310ce6bfd5af0c83e65ef10b608b885b3bb69863c01fb2fcff98"},
+ {file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8b68295808893a81e0a1dbc2274c30dd90880f14d23078e8eb4325ee615fc68"},
+ {file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b01061d377d1944eb67bc40bef5d4d2f762c6ab01598efd9297ce5d0047eb1b5"},
+ {file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9d12c8390f156745e533d01b30773b9753e41d8bbf8bf9dac4b97628cdf16314"},
+ {file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:48825c9f967f922061329d1481b70e9fee937fc68322d6979bc623f69f75bc91"},
+ {file = "levenshtein-0.26.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8ec137170b95736842f99c0e7a9fd8f5641d0c1b63b08ce027198545d983e2b"},
+ {file = "levenshtein-0.26.1-cp311-cp311-win32.whl", hash = "sha256:798f2b525a2e90562f1ba9da21010dde0d73730e277acaa5c52d2a6364fd3e2a"},
+ {file = "levenshtein-0.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:55b1024516c59df55f1cf1a8651659a568f2c5929d863d3da1ce8893753153bd"},
+ {file = "levenshtein-0.26.1-cp311-cp311-win_arm64.whl", hash = "sha256:e52575cbc6b9764ea138a6f82d73d3b1bc685fe62e207ff46a963d4c773799f6"},
+ {file = "levenshtein-0.26.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc741ca406d3704dc331a69c04b061fc952509a069b79cab8287413f434684bd"},
+ {file = "levenshtein-0.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:821ace3b4e1c2e02b43cf5dc61aac2ea43bdb39837ac890919c225a2c3f2fea4"},
+ {file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92694c9396f55d4c91087efacf81297bef152893806fc54c289fc0254b45384"},
+ {file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51ba374de7a1797d04a14a4f0ad3602d2d71fef4206bb20a6baaa6b6a502da58"},
+ {file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7aa5c3327dda4ef952769bacec09c09ff5bf426e07fdc94478c37955681885b"},
+ {file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e2517e8d3c221de2d1183f400aed64211fcfc77077b291ed9f3bb64f141cdc"},
+ {file = "levenshtein-0.26.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9092b622765c7649dd1d8af0f43354723dd6f4e570ac079ffd90b41033957438"},
+ {file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc16796c85d7d8b259881d59cc8b5e22e940901928c2ff6924b2c967924e8a0b"},
+ {file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4370733967f5994ceeed8dc211089bedd45832ee688cecea17bfd35a9eb22b9"},
+ {file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3535ecfd88c9b283976b5bc61265855f59bba361881e92ed2b5367b6990c93fe"},
+ {file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:90236e93d98bdfd708883a6767826fafd976dac8af8fc4a0fb423d4fa08e1bf0"},
+ {file = "levenshtein-0.26.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04b7cabb82edf566b1579b3ed60aac0eec116655af75a3c551fee8754ffce2ea"},
+ {file = "levenshtein-0.26.1-cp312-cp312-win32.whl", hash = "sha256:ae382af8c76f6d2a040c0d9ca978baf461702ceb3f79a0a3f6da8d596a484c5b"},
+ {file = "levenshtein-0.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd091209798cfdce53746f5769987b4108fe941c54fb2e058c016ffc47872918"},
+ {file = "levenshtein-0.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:7e82f2ea44a81ad6b30d92a110e04cd3c8c7c6034b629aca30a3067fa174ae89"},
+ {file = "levenshtein-0.26.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:790374a9f5d2cbdb30ee780403a62e59bef51453ac020668c1564d1e43438f0e"},
+ {file = "levenshtein-0.26.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b05c0415c386d00efda83d48db9db68edd02878d6dbc6df01194f12062be1bb"},
+ {file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3114586032361722ddededf28401ce5baf1cf617f9f49fb86b8766a45a423ff"},
+ {file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2532f8a13b68bf09f152d906f118a88da2063da22f44c90e904b142b0a53d534"},
+ {file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:219c30be6aa734bf927188d1208b7d78d202a3eb017b1c5f01ab2034d2d4ccca"},
+ {file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397e245e77f87836308bd56305bba630010cd8298c34c4c44bd94990cdb3b7b1"},
+ {file = "levenshtein-0.26.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeff6ea3576f72e26901544c6c55c72a7b79b9983b6f913cba0e9edbf2f87a97"},
+ {file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a19862e3539a697df722a08793994e334cd12791e8144851e8a1dee95a17ff63"},
+ {file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dc3b5a64f57c3c078d58b1e447f7d68cad7ae1b23abe689215d03fc434f8f176"},
+ {file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bb6c7347424a91317c5e1b68041677e4c8ed3e7823b5bbaedb95bffb3c3497ea"},
+ {file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b817376de4195a207cc0e4ca37754c0e1e1078c2a2d35a6ae502afde87212f9e"},
+ {file = "levenshtein-0.26.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b50c3620ff47c9887debbb4c154aaaac3e46be7fc2e5789ee8dbe128bce6a17"},
+ {file = "levenshtein-0.26.1-cp313-cp313-win32.whl", hash = "sha256:9fb859da90262eb474c190b3ca1e61dee83add022c676520f5c05fdd60df902a"},
+ {file = "levenshtein-0.26.1-cp313-cp313-win_amd64.whl", hash = "sha256:8adcc90e3a5bfb0a463581d85e599d950fe3c2938ac6247b29388b64997f6e2d"},
+ {file = "levenshtein-0.26.1-cp313-cp313-win_arm64.whl", hash = "sha256:c2599407e029865dc66d210b8804c7768cbdbf60f061d993bb488d5242b0b73e"},
+ {file = "levenshtein-0.26.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc54ced948fc3feafce8ad4ba4239d8ffc733a0d70e40c0363ac2a7ab2b7251e"},
+ {file = "levenshtein-0.26.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6516f69213ae393a220e904332f1a6bfc299ba22cf27a6520a1663a08eba0fb"},
+ {file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4cfea4eada1746d0c75a864bc7e9e63d4a6e987c852d6cec8d9cb0c83afe25b"},
+ {file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a323161dfeeac6800eb13cfe76a8194aec589cd948bcf1cdc03f66cc3ec26b72"},
+ {file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c23e749b68ebc9a20b9047317b5cd2053b5856315bc8636037a8adcbb98bed1"},
+ {file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f80dd7432d4b6cf493d012d22148db7af769017deb31273e43406b1fb7f091c"},
+ {file = "levenshtein-0.26.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ae7cd6e4312c6ef34b2e273836d18f9fff518d84d823feff5ad7c49668256e0"},
+ {file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dcdad740e841d791b805421c2b20e859b4ed556396d3063b3aa64cd055be648c"},
+ {file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e07afb1613d6f5fd99abd4e53ad3b446b4efaa0f0d8e9dfb1d6d1b9f3f884d32"},
+ {file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f1add8f1d83099a98ae4ac472d896b7e36db48c39d3db25adf12b373823cdeff"},
+ {file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1010814b1d7a60833a951f2756dfc5c10b61d09976ce96a0edae8fecdfb0ea7c"},
+ {file = "levenshtein-0.26.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33fa329d1bb65ce85e83ceda281aea31cee9f2f6e167092cea54f922080bcc66"},
+ {file = "levenshtein-0.26.1-cp39-cp39-win32.whl", hash = "sha256:488a945312f2f16460ab61df5b4beb1ea2254c521668fd142ce6298006296c98"},
+ {file = "levenshtein-0.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:9f942104adfddd4b336c3997050121328c39479f69de702d7d144abb69ea7ab9"},
+ {file = "levenshtein-0.26.1-cp39-cp39-win_arm64.whl", hash = "sha256:c1d8f85b2672939f85086ed75effcf768f6077516a3e299c2ba1f91bc4644c22"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6cf8f1efaf90ca585640c5d418c30b7d66d9ac215cee114593957161f63acde0"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d5b2953978b8c158dd5cd93af8216a5cfddbf9de66cf5481c2955f44bb20767a"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b952b3732c4631c49917d4b15d78cb4a2aa006c1d5c12e2a23ba8e18a307a055"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07227281e12071168e6ae59238918a56d2a0682e529f747b5431664f302c0b42"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8191241cd8934feaf4d05d0cc0e5e72877cbb17c53bbf8c92af9f1aedaa247e9"},
+ {file = "levenshtein-0.26.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9e70d7ee157a9b698c73014f6e2b160830e7d2d64d2e342fefc3079af3c356fc"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0eb3059f826f6cb0a5bca4a85928070f01e8202e7ccafcba94453470f83e49d4"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:6c389e44da12d6fb1d7ba0a709a32a96c9391e9be4160ccb9269f37e040599ee"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e9de292f2c51a7d34a0ae23bec05391b8f61f35781cd3e4c6d0533e06250c55"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d87215113259efdca8716e53b6d59ab6d6009e119d95d45eccc083148855f33"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f00a3eebf68a82fb651d8d0e810c10bfaa60c555d21dde3ff81350c74fb4c2"},
+ {file = "levenshtein-0.26.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b3554c1b59de63d05075577380340c185ff41b028e541c0888fddab3c259a2b4"},
+ {file = "levenshtein-0.26.1.tar.gz", hash = "sha256:0d19ba22330d50609b2349021ec3cf7d905c6fe21195a2d0d876a146e7ed2575"},
+]
+
+[package.dependencies]
+rapidfuzz = ">=3.9.0,<4.0.0"
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+ {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+ {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+ {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+ {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+ {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+ {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+ {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "prisma"
+version = "0.15.0"
+description = "Prisma Client Python is an auto-generated and fully type-safe database client"
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "prisma-0.15.0-py3-none-any.whl", hash = "sha256:de949cc94d3d91243615f22ff64490aa6e2d7cb81aabffce53d92bd3977c09a4"},
+ {file = "prisma-0.15.0.tar.gz", hash = "sha256:5cd6402aa8322625db3fc1152040404e7fc471fe7f8fa3a314fa8a99529ca107"},
+]
+
+[package.dependencies]
+click = ">=7.1.2"
+httpx = ">=0.19.0"
+jinja2 = ">=2.11.2"
+nodeenv = "*"
+pydantic = ">=1.10.0,<3"
+python-dotenv = ">=0.12.0"
+StrEnum = {version = "*", markers = "python_version < \"3.11\""}
+tomlkit = "*"
+typing-extensions = ">=4.5.0"
+
+[package.extras]
+all = ["nodejs-bin"]
+node = ["nodejs-bin"]
+
+[[package]]
+name = "prometheus-client"
+version = "0.20.0"
+description = "Python client for the Prometheus monitoring system."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"},
+ {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"},
+]
+
+[package.extras]
+twisted = ["twisted"]
+
+[[package]]
+name = "prometheus-fastapi-instrumentator"
+version = "7.0.0"
+description = "Instrument your FastAPI with Prometheus metrics."
+optional = false
+python-versions = ">=3.8.1,<4.0.0"
+files = [
+ {file = "prometheus_fastapi_instrumentator-7.0.0-py3-none-any.whl", hash = "sha256:96030c43c776ee938a3dae58485ec24caed7e05bfc60fe067161e0d5b5757052"},
+ {file = "prometheus_fastapi_instrumentator-7.0.0.tar.gz", hash = "sha256:5ba67c9212719f244ad7942d75ded80693b26331ee5dfc1e7571e4794a9ccbed"},
+]
+
+[package.dependencies]
+prometheus-client = ">=0.8.0,<1.0.0"
+starlette = ">=0.30.0,<1.0.0"
+
+[[package]]
+name = "pydantic"
+version = "2.8.2"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
+ {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.20.1"
+typing-extensions = [
+ {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
+ {version = ">=4.6.1", markers = "python_version < \"3.13\""},
+]
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.20.1"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
+ {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
+ {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
+ {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
+ {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
+ {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
+ {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
+ {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
+ {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
+ {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
+ {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
+ {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
+ {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
+ {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
+ {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
+ {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
+ {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
+ {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
+ {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
+ {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
+ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
+ {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pyjwt"
+version = "2.9.0"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
+ {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
+[[package]]
+name = "pyright"
+version = "1.1.390"
+description = "Command line wrapper for pyright"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyright-1.1.390-py3-none-any.whl", hash = "sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110"},
+ {file = "pyright-1.1.390.tar.gz", hash = "sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d"},
+]
+
+[package.dependencies]
+nodeenv = ">=1.6.0"
+typing-extensions = ">=4.1"
+
+[package.extras]
+all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"]
+dev = ["twine (>=3.4.1)"]
+nodejs = ["nodejs-wheel-binaries"]
+
+[[package]]
+name = "pytest"
+version = "8.3.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
+ {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=1.5,<2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.25.0"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
+ {file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
+]
+
+[package.dependencies]
+pytest = ">=8.2,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-watcher"
+version = "0.4.3"
+description = "Automatically rerun your tests on file modifications"
+optional = false
+python-versions = "<4.0.0,>=3.7.0"
+files = [
+ {file = "pytest_watcher-0.4.3-py3-none-any.whl", hash = "sha256:d59b1e1396f33a65ea4949b713d6884637755d641646960056a90b267c3460f9"},
+ {file = "pytest_watcher-0.4.3.tar.gz", hash = "sha256:0cb0e4661648c8c0ff2b2d25efa5a8e421784b9e4c60fcecbf9b7c30b2d731b3"},
+]
+
+[package.dependencies]
+tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
+watchdog = ">=2.0.0"
+
+[[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 = "python-levenshtein"
+version = "0.26.1"
+description = "Python extension for computing string edit distances and similarities."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef"},
+ {file = "python_levenshtein-0.26.1.tar.gz", hash = "sha256:24ba578e28058ebb4afa2700057e1678d7adf27e43cd1f17700c09a9009d5d3a"},
+]
+
+[package.dependencies]
+Levenshtein = "0.26.1"
+
+[[package]]
+name = "rapidfuzz"
+version = "3.9.5"
+description = "rapid fuzzy string matching"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "rapidfuzz-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7659058863d84a2c36c5a76c28bc8713d33eab03e677e67260d9e1cca43fc3bb"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:802a018776bd3cb7c5d23ba38ebbb1663a9f742be1d58e73b62d8c7cace6e607"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da71e8fdb0d1a21f4b58b2c84bcbc2b89a472c073c5f7bdb9339f4cb3122c0e3"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9433cb12731167b358fbcff9828d2294429986a03222031f6d14308eb643c77"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e33e1d185206730b916b3e7d9bce1941c65b2a1488cdd0457ae21be385a7912"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:758719e9613c47a274768f1926460955223fe0a03e7eda264f2b78b1b97a4743"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7981cc6240d01d4480795d758ea2ee748257771f68127d630045e58fe1b5545a"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b6cdca86120c3f9aa069f8d4e1c5422e92f833d705d719a2ba7082412f4c933b"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ffa533acb1a9dcb6e26c4467fdc1347995fb168ec9f794b97545f6b72dee733c"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:13eeaeb0d5fe00fa99336f73fb5ab65c46109c7121cf87659b9601908b8b6178"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d7b1922b1403ccb3583218e8cd931b08e04c5442ca03dbaf6ea4fcf574ee2b24"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b0189f691cea4dc9fe074ea6b97da30a91d0882fa69724b4b34b51d2c1983473"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-win32.whl", hash = "sha256:72e466e5de12a327a09ed6d0116f024759b5146b335645c32241da84134a7f34"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:345011cfcafaa3674c46673baad67d2394eb58285530d8333e65c3c9a143b4f4"},
+ {file = "rapidfuzz-3.9.5-cp310-cp310-win_arm64.whl", hash = "sha256:5dc19c8222475e4f7f528b94d2fa28e7979355c5cf7c6e73902d2abb2be96522"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c741972d64031535cfd76d89cf47259e590e822353be57ec2f5d56758c98296"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7452d079800cf70a7314f73044f03cbcbd90a651d9dec39443d2a8a2b63ab53"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06f163a0341bad162e972590b73e17f9cea2ed8ee27b193875ccbc3dd6eca2f"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:529e2cf441746bd492f6c36a38bf9fa6a418df95b9c003f8e92a55d8a979bd9c"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9811a741aa1350ad36689d675ded8b34e423e68b396bd30bff751a9c582f586e"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e36c4640a789b8c922b69a548968939d1c0433fa7aac83cb08e1334d4e5d7de"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53fb2f32f14c921d2f673c5b7cd58d4cc626c574a28c0791f283880d8e57022c"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:031806eb035a6f09f4ff23b9d971d50b30b5e93aa3ee620c920bee1dc32827e7"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6dbe1df0b9334e3cf07445d810c81734ae23d137b5efc69e1d676ff55691351"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:24345826b50aafcea26e2e4be5c103d96fe9d7fc549ac9190641300290958f3b"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bfd3b66ee1f0ebb40c672a7a7e5bda00fb763fa9bca082058084175151f8e685"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6f1df5b0e602e94199cccb5e241bbc2319644003e34f077741ebf48aea7ed1a"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-win32.whl", hash = "sha256:f080d6709f51a8335e73826b96af9b4e3657631eca6c69e1ac501868dcc84b7f"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bf9ed6988da6a2c1f8df367cb5d6be26a3d8543646c8eba79741ac9e764fbc59"},
+ {file = "rapidfuzz-3.9.5-cp311-cp311-win_arm64.whl", hash = "sha256:599714790dfac0a23a473134e6677d0a103690a4e21ba189cfc826e322cdc8d5"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9729852038fb2de096e249899f8a9bee90fb1f92e10b6ccc539d5bb798c703bc"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9dc39435476fb3b3b3c24ab2c08c726056b2b487aa7ee450aee698b808c808ac"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6ceea632b0eb97dac54411c29feb190054e91fd0571f585b56e4a9159c55ab0"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cadd66e6ef9901909dc1b11db91048f1bf4613ba7d773386f922e28b1e1df4da"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63e34fb3586431589a5e1cd7fc61c6f057576c6c6804c1c673bac3de0516dee7"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:181073256faec68e6b8ab3329a36cfa1360f7906aa70d9aee4a39cb70889f73f"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8419c18bbbd67058ca1312f35acda2e4e4592650f105cfd166569a2ebccd01f1"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:191d1057cca56641f7b919fe712cb7e48cd226342e097a78136127f8bde32caa"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fe5a11eefd0ae90d32d9ff706a894498b4efb4b0c263ad9d1e6401050863504d"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b024d9d69bb83e125adee4162991f2764f16acc3fb1ed0f0fc1ad5aeb7e394"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d5a34b8388ae99bdbd5a3646f45ac318f4c870105bdbe42a2f4c85e5b347761"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e09abc0d397019bba61c8e6dfe2ec863d4dfb1762f51c9197ce0af5d5fd9adb"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-win32.whl", hash = "sha256:e3c4be3057472c79ba6f4eab35daa9f12908cb697c472d05fbbd47949a87aec6"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:0d9fdb74df87018dd4146f3d00df9fca2c27f060936a9e8d3015e7bfb9cb69e4"},
+ {file = "rapidfuzz-3.9.5-cp312-cp312-win_arm64.whl", hash = "sha256:491d3d425b5fe3f61f3b9a70abfd498ce9139d94956db7a8551e537e017c0e57"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:518dec750a30f115ba1299ef2547cf468a69f310581a030c8a875257de747c5f"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:252dc3d1c3d613b8db1b59d13381937e420c99f8a351ffa0e78c2f54746e107f"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd17688b75b6fa983e8586cad30f36eb9736b860946cc8b633b9442c9481831"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8032492021b0aa55a623d6f6e739a5d4aaabc32af379c2a5656bf1e9e178bf1"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73362eb1c3d02f32e4c7f0d77eb284e9a13f278cff224f71e8f60e2aff5b6a5d"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a42d1f7b8988f50013e703ed27b5e216ef8a725b2f4ac53754ad0476020b26f4"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4f2e985172bb76c9179e11fb67d9c9ecbee4933740eca2977797094df02498d"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e943c5cbd10e15369be1f371ef303cb413c1008f64d93bd13762ea06ca84d59"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0d34b0e8e29f80cb2ac8afe8fb7b01a542b136ffbf7e2b9983d11bce49398f68"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:62b8f9f58e9dffaa86fef84db2705457a58e191a962124f2b815026ba79d9aba"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ebf682bdb0f01b6b1f9a9ffe918aa3ac84fbdadb998ffbfcd5f9b12bd280170f"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3ed0c17e5b6fdd2ac3230bdefa908579971377c36aa4a2f132700fa8145040db"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-win32.whl", hash = "sha256:ac460d89b9759e37eef23fed05184179654882a241f6b2363df194f8940cc55f"},
+ {file = "rapidfuzz-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:cf9aceb4227fd09f9a20e505f78487b2089d6420ce232d288522ea0a78b986b9"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14587df847d0d50bd10cde0a198b5d64eedb7484c72b825f5c2ead6e6ff16eee"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd94d952299ec73ea63a0fa4b699a2750785b6bb82aa56fd886d9023b86f90ab"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:733bf3d7876bf6d8167e6436f99d6ea16a218ec2c8eb9da6048f20b9cc8733e2"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb28f2b7173ed3678b4630b0c8b21503087d1cd082bae200dc2519ca38b26686"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a4c8a2c5ae4b133fec6b5db1af9a4126ffa6eca18a558fe5b6ab8e330d3d78"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5feb75e905281e5c669e21c98d594acc3b222a8694d9342f17df988766d83748"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d047b01637a31d9bf776b66438f574fd1db856ad14cf296c1f48bb6bef8a5aff"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9e0a656274ac75ec24499a06c0bc5eee67bcd8276c6061da7c05d549f1b1a61"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:16c982dd3cdd33cf4aac91027a263a081d1a8050dc33a27470367a391a8d1576"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a0c878d0980508e90e973a9cbfb591acc370085f2301c6aacadbd8362d52a36"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1d9bcfec5efd55b6268328cccd12956d833582d8da6385231a5c6c6201a1156a"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8171fc6e4645e636161a9ef5b44b20605adbefe23cd990b68d72cae0b9c12509"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-win32.whl", hash = "sha256:35088e759b083398ab3c4154517476e116653b7403604677af9a894179f1042f"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:6d8cc7e6e5c6fbcacdfe3cf7a86b60dcaf216216d86e6879ff52d488e5b11e27"},
+ {file = "rapidfuzz-3.9.5-cp39-cp39-win_arm64.whl", hash = "sha256:506547889f18db0acca787ffb9f287757cbfe9f0fadddd4e07c64ce0bd924e13"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f4e0122603af2119579e9f94e172c6e460860fdcdb713164332c1951c13df999"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e46cd486289d1d8e3dab779c725f5dde77b286185d32e7b874bfc3d161e3a927"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e2c0c8bbe4f4525009e3ad9b94a39cdff5d6378233e754d0b13c29cdfaa75fc"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb47513a17c935f6ee606dcae0ea9d20a3fb0fe9ca597758472ea08be62dc54"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976ed1105a76935b6a4d2bbc7d577be1b97b43997bcec2f29a0ab48ff6f5d6b1"},
+ {file = "rapidfuzz-3.9.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9cf2028edb9ccd21d1d5aaacef2fe3e14bee4343df1c2c0f7373ef6e81013bef"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:926701c8e61319ee2e4888619143f58ddcc0e3e886668269b8e053f2d68c1e92"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:99eaa8dd8a44664813e0bef014775993ef75a134a863bc54cd855a60622203fd"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7508ef727ef4891141dd3ac7a39a2327384ece070521ac9c58f06c27d57c72d5"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f33d05db5bba1d076446c51347a6d93ff24d8f9d01b0b8b15ca8ec8b1ef382"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7252666b85c931d51a59d5308bb6827a67434917ef510747d3ce7e88ec17e7f2"},
+ {file = "rapidfuzz-3.9.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d26f7299e2872d18fb7df1bc043e53aa94fc5a4a2a6a9537ad8707579fcb1668"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2b17ecc17322b659962234799e90054e420911b8ca510a7869c2f4419f9f3ecb"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f3e037b9ec621dec0157d81566e7d47a91405e379335cf8f4ed3c20d61db91d8"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c4d1ba2647c8d2a82313c4dde332de750c936b94f016308339e762c2e5e53d"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:876e663b11d9067e1096ea76a2de87227c7b513aff2b60667b20417da74183e4"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adee55488490375c1604b878fbc1eb1a51fe5e6f5bd05047df2f8c6505a48728"},
+ {file = "rapidfuzz-3.9.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:abb1ac683671000bd4ec215a494aba687d75a198db72188408154a19ea313ff4"},
+ {file = "rapidfuzz-3.9.5.tar.gz", hash = "sha256:257f2406a671371bafd99a2a2c57f991783446bc2176b93a83d1d833e35d36df"},
+]
+
+[package.extras]
+full = ["numpy"]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+ {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruff"
+version = "0.8.3"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
+ {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
+ {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
+ {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
+ {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
+ {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
+ {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
+ {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
+ {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
+ {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
+ {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
+ {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
+]
+
+[[package]]
+name = "sentry-sdk"
+version = "2.19.2"
+description = "Python client for Sentry (https://sentry.io)"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"},
+ {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"},
+]
+
+[package.dependencies]
+certifi = "*"
+fastapi = {version = ">=0.79.0", optional = true, markers = "extra == \"fastapi\""}
+urllib3 = ">=1.26.11"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.5)"]
+anthropic = ["anthropic (>=0.16)"]
+arq = ["arq (>=0.23)"]
+asyncpg = ["asyncpg (>=0.23)"]
+beam = ["apache-beam (>=2.12)"]
+bottle = ["bottle (>=0.12.13)"]
+celery = ["celery (>=3)"]
+celery-redbeat = ["celery-redbeat (>=2)"]
+chalice = ["chalice (>=1.16.0)"]
+clickhouse-driver = ["clickhouse-driver (>=0.2.0)"]
+django = ["django (>=1.8)"]
+falcon = ["falcon (>=1.4)"]
+fastapi = ["fastapi (>=0.79.0)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
+grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
+http2 = ["httpcore[http2] (==1.*)"]
+httpx = ["httpx (>=0.16.0)"]
+huey = ["huey (>=2)"]
+huggingface-hub = ["huggingface_hub (>=0.22)"]
+langchain = ["langchain (>=0.0.210)"]
+launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"]
+litestar = ["litestar (>=2.0.0)"]
+loguru = ["loguru (>=0.5)"]
+openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
+openfeature = ["openfeature-sdk (>=0.7.1)"]
+opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
+opentelemetry-experimental = ["opentelemetry-distro"]
+pure-eval = ["asttokens", "executing", "pure_eval"]
+pymongo = ["pymongo (>=3.1)"]
+pyspark = ["pyspark (>=2.4.4)"]
+quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
+rq = ["rq (>=0.6)"]
+sanic = ["sanic (>=0.8)"]
+sqlalchemy = ["sqlalchemy (>=1.2)"]
+starlette = ["starlette (>=0.19.1)"]
+starlite = ["starlite (>=1.48)"]
+tornado = ["tornado (>=6)"]
+
+[[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.41.2"
+description = "The little ASGI library that shines."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d"},
+ {file = "starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62"},
+]
+
+[package.dependencies]
+anyio = ">=3.4.0,<5"
+
+[package.extras]
+full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
+
+[[package]]
+name = "strenum"
+version = "0.4.15"
+description = "An Enum that inherits from str."
+optional = false
+python-versions = "*"
+files = [
+ {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"},
+ {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"},
+]
+
+[package.extras]
+docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"]
+release = ["twine"]
+test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.13.0"
+description = "Style preserving TOML library"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"},
+ {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.2.2"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
+ {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "uvicorn"
+version = "0.34.0"
+description = "The lightning-fast ASGI server."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
+ {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
+]
+
+[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.6.3)", "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 = "watchdog"
+version = "4.0.1"
+description = "Filesystem events monitoring"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
+ {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
+ {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
+ {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
+ {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
+ {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
+ {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
+ {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
+ {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
+ {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
+ {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "^3.10"
+content-hash = "5dbf6cd95ba8e80c4a6b4e6a54c6cdfb1488619e4293d1d5a8572c5330485493"
diff --git a/autogpt_platform/market/pyproject.toml b/autogpt_platform/market/pyproject.toml
new file mode 100644
index 000000000..279a11116
--- /dev/null
+++ b/autogpt_platform/market/pyproject.toml
@@ -0,0 +1,57 @@
+[tool.poetry]
+name = "market"
+version = "0.1.0"
+description = ""
+authors = [
+ "SwiftyOS ",
+ "Nicholas Tindle ",
+]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+prisma = "^0.15.0"
+python-dotenv = "^1.0.1"
+uvicorn = "^0.34.0"
+fastapi = "^0.115.6"
+sentry-sdk = { extras = ["fastapi"], version = "^2.19.2" }
+fuzzywuzzy = "^0.18.0"
+python-levenshtein = "^0.26.1"
+# autogpt-platform-backend = { path = "../backend", develop = true }
+prometheus-fastapi-instrumentator = "^7.0.0"
+
+
+autogpt-libs = {path = "../autogpt_libs"}
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.3.4"
+pytest-asyncio = "^0.25.0"
+
+pytest-watcher = "^0.4.3"
+requests = "^2.32.3"
+ruff = "^0.8.3"
+pyright = "^1.1.390"
+isort = "^5.13.2"
+black = "^24.10.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.scripts]
+format = "scripts:format"
+lint = "scripts:lint"
+app = "scripts:app"
+setup = "scripts:setup"
+populate = "scripts:populate_database"
+
+[tool.pytest-watcher]
+now = false
+clear = true
+delay = 0.2
+runner = "pytest"
+runner_args = []
+patterns = ["*.py"]
+ignore_patterns = []
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
diff --git a/autogpt_platform/market/schema.prisma b/autogpt_platform/market/schema.prisma
new file mode 100644
index 000000000..8a29c2b2a
--- /dev/null
+++ b/autogpt_platform/market/schema.prisma
@@ -0,0 +1,80 @@
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+generator client {
+ provider = "prisma-client-py"
+ recursive_type_depth = 5
+ interface = "asyncio"
+ previewFeatures = ["fullTextSearch"]
+ partial_type_generator = "market/utils/partial_types.py"
+}
+
+enum SubmissionStatus {
+ PENDING
+ APPROVED
+ REJECTED
+}
+
+model Agents {
+ id String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ version Int @default(1)
+
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ submissionDate DateTime @default(now())
+ submissionReviewDate DateTime?
+ submissionStatus SubmissionStatus @default(PENDING)
+ submissionReviewComments String?
+
+ name String?
+ description String?
+ author String?
+
+ keywords String[]
+ categories String[]
+ search Unsupported("tsvector")? @default(dbgenerated("''::tsvector"))
+
+ graph Json
+ AnalyticsTracker AnalyticsTracker[]
+ FeaturedAgent FeaturedAgent?
+ InstallTracker InstallTracker[]
+
+ @@id(name: "graphVersionId", [id, version])
+}
+
+model AnalyticsTracker {
+ id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ agentId String @unique @db.Uuid
+ agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
+ views Int
+ downloads Int
+}
+
+enum InstallationLocation {
+ LOCAL
+ CLOUD
+}
+
+model InstallTracker {
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ marketplaceAgentId String @db.Uuid
+ marketplaceAgent Agents @relation(fields: [marketplaceAgentId], references: [id], onDelete: Cascade)
+ installedAgentId String @db.Uuid
+ installationLocation InstallationLocation
+ createdAt DateTime @default(now())
+
+ @@unique([marketplaceAgentId, installedAgentId])
+}
+
+model FeaturedAgent {
+ id String @id @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
+ agentId String @unique @db.Uuid
+ agent Agents @relation(fields: [agentId], references: [id], onDelete: Cascade)
+ isActive Boolean @default(false)
+ featuredCategories String[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/autogpt_platform/market/scripts.py b/autogpt_platform/market/scripts.py
new file mode 100644
index 000000000..5465007b7
--- /dev/null
+++ b/autogpt_platform/market/scripts.py
@@ -0,0 +1,65 @@
+import os
+import subprocess
+
+directory = os.path.dirname(os.path.realpath(__file__))
+
+
+def run(*command: str) -> None:
+ print(f">>>>> Running poetry run {' '.join(command)}")
+ subprocess.run(["poetry", "run"] + list(command), cwd=directory, check=True)
+
+
+def lint():
+ try:
+ run("ruff", "check", ".", "--exit-zero")
+ run("isort", "--diff", "--check", "--profile", "black", ".")
+ run("black", "--diff", "--check", ".")
+ run("pyright")
+ except subprocess.CalledProcessError as e:
+ print("Lint failed, try running `poetry run format` to fix the issues: ", e)
+ raise e
+
+
+def populate_database():
+ import glob
+ import json
+ import pathlib
+
+ import requests
+
+ import market.model
+
+ templates = pathlib.Path(__file__).parent.parent / "graph_templates"
+
+ all_files = glob.glob(str(templates / "*.json"))
+
+ for file in all_files:
+ with open(file, "r") as f:
+ data = f.read()
+ req = market.model.AddAgentRequest(
+ graph=json.loads(data),
+ author="Populate DB",
+ categories=["Pre-Populated"],
+ keywords=["test"],
+ )
+ response = requests.post(
+ "http://localhost:8015/api/v1/market/admin/agent", json=req.model_dump()
+ )
+ print(response.text)
+
+
+def format():
+ run("ruff", "check", "--fix", ".")
+ run("isort", "--profile", "black", ".")
+ run("black", ".")
+ run("pyright", ".")
+
+
+def app():
+ port = os.getenv("PORT", "8015")
+ run("uvicorn", "market.app:app", "--reload", "--port", port, "--host", "0.0.0.0")
+
+
+def setup():
+ run("prisma", "generate")
+ run("prisma", "migrate", "deploy")
diff --git a/autogpt_platform/market/tests/test_agents.py b/autogpt_platform/market/tests/test_agents.py
new file mode 100644
index 000000000..bcf9f033f
--- /dev/null
+++ b/autogpt_platform/market/tests/test_agents.py
@@ -0,0 +1,79 @@
+from datetime import datetime, timezone
+
+import pytest
+from fastapi.testclient import TestClient
+
+from market.app import app
+from market.db import AgentQueryError
+
+
+@pytest.fixture
+def test_client():
+ return TestClient(app)
+
+
+# Mock data
+mock_agents = [
+ {
+ "id": "1",
+ "name": "Agent 1",
+ "description": "Description 1",
+ "author": "Author 1",
+ "keywords": ["AI", "chatbot"],
+ "categories": ["general"],
+ "version": 1,
+ "createdAt": datetime.now(timezone.utc),
+ "updatedAt": datetime.now(timezone.utc),
+ "graph": {"node1": "value1"},
+ },
+ {
+ "id": "2",
+ "name": "Agent 2",
+ "description": "Description 2",
+ "author": "Author 2",
+ "keywords": ["ML", "NLP"],
+ "categories": ["specialized"],
+ "version": 1,
+ "createdAt": datetime.now(timezone.utc),
+ "updatedAt": datetime.now(timezone.utc),
+ "graph": {"node2": "value2"},
+ },
+]
+
+
+# TODO: Need to mock prisma somehow
+
+
+@pytest.mark.asyncio
+async def test_list_agents(test_client):
+ response = test_client.get("/agents")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data["agents"]) == 2
+ assert data["total_count"] == 2
+
+
+@pytest.mark.asyncio
+async def test_list_agents_with_filters(test_client):
+ response = await test_client.get("/agents?name=Agent 1&keyword=AI&category=general")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data["agents"]) == 1
+ assert data["agents"][0]["name"] == "Agent 1"
+
+
+@pytest.mark.asyncio
+async def test_get_agent_details(test_client, mock_get_agent_details):
+ response = await test_client.get("/agents/1")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["id"] == "1"
+ assert data["name"] == "Agent 1"
+ assert "graph" in data
+
+
+@pytest.mark.asyncio
+async def test_get_nonexistent_agent(test_client, mock_get_agent_details):
+ mock_get_agent_details.side_effect = AgentQueryError("Agent not found")
+ response = await test_client.get("/agents/999")
+ assert response.status_code == 404