diff --git a/.editorconfig b/.editorconfig index 26f53fd..c27a17b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ indent_size = 2 [*.md] trim_trailing_whitespace = false +indent_size = 4 [Makefile] indent_style = tab diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index 10eb0d6..b5a19bb 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -1,10 +1,12 @@ from typing import List +from django.http import Http404 from django.shortcuts import get_object_or_404 from langchain_core.messages import message_to_dict from ninja import NinjaAPI from ninja.operation import Operation +from ninja.security import django_auth from django_ai_assistant import package_name, version from django_ai_assistant.api.schemas import ( @@ -26,7 +28,14 @@ def get_openapi_operation_id(self, operation: Operation) -> str: return (package_name + "_" + name).replace(".", "_") -api = API(title=package_name, version=version, urls_namespace="django_ai_assistant") +api = API( + title=package_name, + version=version, + urls_namespace="django_ai_assistant", + # Add auth to all endpoints + auth=django_auth, + csrf=True, +) @api.exception_handler(AIUserNotAllowedError) @@ -63,7 +72,12 @@ def create_thread(request, payload: ThreadSchemaIn): @api.get("threads/{thread_id}/", response=ThreadSchema, url_name="thread_detail_update_delete") def get_thread(request, thread_id: str): - thread = use_cases.get_single_thread(thread_id=thread_id, user=request.user, request=request) + try: + thread = use_cases.get_single_thread( + thread_id=thread_id, user=request.user, request=request + ) + except Thread.DoesNotExist: + raise Http404("No %s matches the given query." % Thread._meta.object_name) from None return thread diff --git a/django_ai_assistant/helpers/assistants.py b/django_ai_assistant/helpers/assistants.py index 7eace1d..42b7059 100644 --- a/django_ai_assistant/helpers/assistants.py +++ b/django_ai_assistant/helpers/assistants.py @@ -192,7 +192,9 @@ def get_contextualize_prompt(self) -> ChatPromptTemplate: return ChatPromptTemplate.from_messages( [ ("system", contextualize_q_system_prompt), + # TODO: make history key confirgurable? MessagesPlaceholder("history"), + # TODO: make input key confirgurable? ("human", "{input}"), ] ) @@ -284,6 +286,15 @@ def invoke(self, *args, thread_id: int | None, **kwargs): chain = self.as_chain(thread_id) return chain.invoke(*args, **kwargs) + def run(self, message, thread_id: int | None, **kwargs): + return self.invoke( + { + "input": message, + }, + thread_id=thread_id, + **kwargs, + )["output"] + def run_as_tool(self, message: str, **kwargs): chain = self.as_chain(thread_id=None) output = chain.invoke({"input": message}, **kwargs) diff --git a/django_ai_assistant/helpers/use_cases.py b/django_ai_assistant/helpers/use_cases.py index 595a424..5fb2405 100644 --- a/django_ai_assistant/helpers/use_cases.py +++ b/django_ai_assistant/helpers/use_cases.py @@ -17,6 +17,7 @@ can_delete_message, can_delete_thread, can_run_assistant, + can_view_thread, ) @@ -98,7 +99,12 @@ def get_single_thread( user: Any, request: HttpRequest | None = None, ): - return Thread.objects.filter(created_by=user).get(id=thread_id) + thread = Thread.objects.get(id=thread_id) + + if not can_view_thread(thread=thread, user=user, request=request): + raise AIUserNotAllowedError("User is not allowed to view this thread") + + return thread def get_threads( diff --git a/django_ai_assistant/permissions.py b/django_ai_assistant/permissions.py index 015330b..5b912c7 100644 --- a/django_ai_assistant/permissions.py +++ b/django_ai_assistant/permissions.py @@ -25,6 +25,20 @@ def can_create_thread( ) +def can_view_thread( + thread: Thread, + user: Any, + request: HttpRequest | None = None, + **kwargs, +) -> bool: + return app_settings.call_fn( + "CAN_VIEW_THREAD_FN", + **_get_default_kwargs(user, request), + thread=thread, + **kwargs, + ) + + def can_update_thread( thread: Thread, user: Any, diff --git a/docs/get-started.md b/docs/get-started.md new file mode 100644 index 0000000..b7bd3fe --- /dev/null +++ b/docs/get-started.md @@ -0,0 +1,32 @@ +# Get started + +## Prerequisites + +- Python: Supported Python versions +- Django: Supported Django versions + +## How to install + +Install Django AI Assistant package: + +```bash +pip install django-ai-assistant +``` + +Add Django AI Assistant to your Django project's `INSTALLED_APPS`: + +```python title="myproject/settings.py" +INSTALLED_APPS = [ + ... + 'django_ai_assistant', + ... +] +``` + +Run the migrations: + +```bash +python manage.py migrate +``` + +Learn how to use the package in the [Tutorial](tutorial.md) section. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..8e3464f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +# Django AI Assistant + +Implement powerful AI Assistants using Django. +Combine the power of Large Language Models with Django's productivity. + +Regardless of the feasibility of AGI, AI assistants are (already!) a new paradigm for computation. +AI agents and assistants allow devs to easily build applications with smart decision logic +that would otherwise be too expensive to build and maintain. + +The latest LLMs from major AI providers have a "killer feature" called Tool Calling, +which enables AI models to call provided methods from Django's side, and essentially +do anything a Django view can, such as accessing DBs, checking permissions, sending emails, +downloading and uploading media files, etc. + +While users commonly interact with LLMs via conversations, AI Assistants can do a lot with any kind of string input, including JSON. +Your application's end users won't even realize that a LLM is doing the heavy-lifting behind the scenes! +Some ideas for innovative AI assistants: + +- A movie recommender chatbot that helps users manage their movie backlogs +- An autofill button for certain forms of your application +- Personalized email reminders that consider users' written preferences and the application's recent notifications +- A real-time audio guide for tourists that recommends attractions given the user's current location + +We have an open-source example with some of those applications, but it's best to start with the [Get Started](get-started.md) guide. diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..49a2a4a --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,396 @@ +# Tutorial + +In this tutorial, you will learn how to use Django AI Assistant to supercharge your Django project with LLM capabilities. + +## Prerequisites + +Make sure you properly configured Django AI Assistant as described in the [Get Started](get-started.md) guide. + +## Setting up API keys + +The tutorial below uses OpenAI's gpt-4o model, so make sure you have `OPENAI_API_KEY` set as an environment variable for your Django project. +You can also use other models, keep reading to learn more. Just make sure their keys are properly set. + +!!! note + An easy way to set environment variables is to use a `.env` file in your project's root directory and use `python-dotenv` to load them. + Our [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) uses this approach. + +## What AI Assistants can do + +AI Assistants are LLMs that can answer to user queries as ChatGPT does, i.e. inputting and outputting strings. +But when integrated with Django, they can also do anything a Django view can, such as accessing the database, +checking permissions, sending emails, downloading and uploading media files, etc. +This is possible by defining "tools" the AI can use. These tools are methods in an AI Assistant class on the Django side. + +## Defining an AI Assistant + +### Registering + +To create an AI Assistant, you need to: + +1. Create an `ai_assistants.py` file; +2. Define a class that inherits from `AIAssistant` with the decorator `@register_assistant` over it; +3. Provide an `id`, a `name`, some `instructions` for the LLM (a system prompt), and a `model` name: + +```python title="myapp/ai_assistants.py" +from django_ai_assistant import AIAssistant, register_assistant + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "gpt-4o" +``` + +### Defining tools + +Useful tools give abilities the LLM doesn't have out-of-the-box, +such as getting the current date and finding the current weather by calling an API. + +Use the `@method_tool` decorator to define a tool method in the AI Assistant: + +```{.python title="myapp/ai_assistants.py" hl_lines="15-22"} +from django.utils import timezone +from django_ai_assistant import AIAssistant, method_tool, register_assistant +import json + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "gpt-4o" + + def get_instructions(self): + return f"{self.instructions} Today is {timezone.now().isoformat()}." + + @method_tool + def get_weather(self, location: str) -> str: + """Fetch the current weather data for a location""" + return json.dumps({ + "location": location, + "temperature": "25°C", + "weather": "sunny" + }) # imagine some weather API here, this is just a placeholder +``` + +The `get_weather` method is a tool that the AI Assistant can use to get the current weather for a location, when the user asks for it. +The tool method must be fully type-hinted (all parameters and return value), and it must include a descriptive docstring. +This is necessary for the LLM model to understand the tool's purpose. + +A conversation with this Weather Assistant looks like this: + +```txt +User: What's the weather in New York City? +AI: The weather in NYC is sunny with a temperature of 25°C. +``` + +!!! note + State of the art models such as gpt-4o can process JSON well. + You can return a `json.dumps(api_output)` from a tool method and the model will be able to process it before responding the user. + +### Using Django logic in tools + +You have access to the current request user in tools: + +```{.python title="myapp/ai_assistants.py" hl_lines=13} +from django_ai_assistant import AIAssistant, method_tool, register_assistant + +@register_assistant +class PersonalAIAssistant(AIAssistant): + id = "personal_assistant" + name = "Personal Assistant" + instructions = "You are a personal assistant." + model = "gpt-4o" + + @method_tool + def get_current_user_username(self) -> str: + """Get the username of the current user""" + return self._user.username +``` + +You can also add any Django logic to tools, such as querying the database: + +```{.python title="myapp/ai_assistants.py" hl_lines=14-16} +from django_ai_assistant import AIAssistant, method_tool, register_assistant +import json + +@register_assistant +class IssueManagementAIAssistant(AIAssistant): + id = "issue_mgmt_assistant" + name = "Issue Management Assistant" + instructions = "You are an issue management bot." + model = "gpt-4o" + + @method_tool + def get_current_user_assigned_issues(self) -> str: + """Get the issues assigned to the current user""" + return json.dumps({ + "issues": list(Issue.objects.filter(assignee=self._user).values()) + }) +``` + +!!! warning + Make sure you only return to the LLM what the user can see, considering permissions and privacy. + Code the tools as if they were Django views. + +### Using pre-implemented tools + +Django AI Assistant works with [any LangChain-compatible tool](https://python.langchain.com/v0.2/docs/integrations/tools/). +Just override the `get_tools` method in your AI Assistant class to include the tools you want to use. + +For example, you can use the `TavilySearch` tool to provide your AI Assistant with the ability to search the web +for information about upcoming movies. + +First install dependencies: + +```bash +pip install -U langchain-community tavily-python +``` + +Then, set the `TAVILY_API_KEY` environment variable. You'll need to sign up at [Tavily](https://tavily.com/). + +Finally, add the tool to your AI Assistant class by overriding the `get_tools` method: + +```{.python title="myapp/ai_assistants.py" hl_lines="2 20"} +from django_ai_assistant import AIAssistant, register_assistant +from langchain_community.tools.tavily_search import TavilySearchResults + +@register_assistant +class MovieSearchAIAssistant(AIAssistant): + id = "movie_search_assistant" # noqa: A003 + instructions = ( + "You're a helpful movie search assistant. " + "Help the user find more information about movies. " + "Use the provided tools to search the web for upcoming movies. " + ) + name = "Movie Search Assistant" + model = "gpt-4o" + + def get_instructions(self): + return f"{self.instructions} Today is {timezone.now().isoformat()}." + + def get_tools(self): + return [ + TavilySearchResults(), + *super().get_tools(), + ] +``` + +!!! note + As of now, Django AI Assistant is powered by [LangChain](https://python.langchain.com/v0.2/docs/introduction/), + but previous knowledge on LangChain is NOT necessary to use this library, at least for the main use cases. + +## Using an AI Assistant + +### Manually calling an AI Assistant + +You can manually call an AI Assistant from anywhere in your Django application: + +```python +from myapp.ai_assistants import WeatherAIAssistant + +assistant = WeatherAIAssistant() +output = assistant.run("What's the weather in New York City?") +assert output == "The weather in NYC is sunny with a temperature of 25°C." +``` + +The constructor of `AIAssistant` receives `user`, `request`, `view` as optional parameters, +which can be used in the tools with `self._user`, `self._request`, `self._view`. +Also, any extra parameters passed in constructor are stored at `self._init_kwargs`. + +### Threads of Messages + +The django-ai-assistant app provides two models `Thread` and `Message` to store and retrieve conversations with AI Assistants. +LLMs are stateless by design, meaning they don't hold any context between calls. All they know is the current input. +But by using the `AIAssistant` class, the conversation state is stored in the database as multiple `Message` of a `Thread`, +and automatically retrieved then passed to the LLM when calling the AI Assistant. + +To create a `Thread`, you can use a helper from the `django_ai_assistant.use_cases` module. For example: + +```{.python hl_lines="4 8"} +from django_ai_assistant.use_cases import create_thread, get_thread_messages +from myapp.ai_assistants import WeatherAIAssistant + +thread = create_thread(name="Weather Chat", user=some_user) +assistant = WeatherAIAssistant() +assistant.run("What's the weather in New York City?", thread_id=thread.id) + +messages = get_thread_messages(thread) # returns both user and AI messages +``` + +More CRUD helpers are available at `django_ai_assistant.use_cases` module. Check the API Reference for more information. + + +### Using built-in API views + +You can use the built-in API views to interact with AI Assistants via HTTP requests from any frontend, +such as a React application or a mobile app. Add the following to your Django project's `urls.py`: + +```python title="myproject/urls.py" +from django.urls import include, path + +urlpatterns = [ + path("ai-assistant/", include("django_ai_assistant.urls")), + ... +] +``` + +The built-in API supports retrieval of Assistants info, as well as CRUD for Threads and Messages. +It has a OpenAPI schema that you can explore at `ai-assistant/docs/`. + +### Configuring permissions + +The API uses the helpers from the `django_ai_assistant.use_cases` module, which have permission checks +to ensure the user can use a certain AI Assistant or do CRUD on Threads and Messages. + +By default, any authenticated user can use any AI Assistant, and create a thread. +Users can manage both their own threads and the messages on them. Therefore, the default permissions are: + +```python title="myproject/settings.py" +AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" +AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_DELETE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_CREATE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_UPDATE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_DELETE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_RUN_ASSISTANT = "django_ai_assistant.permissions.allow_all" +``` + +You can override these settings in your Django project's `settings.py` to customize the permissions. + +Thread permission signatures look like this: + +```python +from django_ai_assistant.models import Thread +from django.http import HttpRequest + +def check_custom_thread_permission( + thread: Thread, + user: Any, + request: HttpRequest | None = None) -> bool: + return ... +``` + +While Message permission signatures look like this: + +```python +from django_ai_assistant.models import Thread, Message +from django.http import HttpRequest + +def check_custom_message_permission( + message: Message, + thread: Thread, + user: Any, + request: HttpRequest | None = None) -> bool: + return ... +``` + +## Advanced usage + +### Using other AI models + +By default the supported models are OpenAI ones, +but you can use [any chat model from Langchain that supports Tool Calling](https://python.langchain.com/v0.2/docs/integrations/chat/#advanced-features) by overriding `get_llm`: + +```python title="myapp/ai_assistants.py" +from django_ai_assistant import AIAssistant, register_assistant +from langchain_anthropic import ChatAnthropic + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "claude-3-opus-20240229" + + def get_llm(self): + model = self.get_model() + temperature = self.get_temperature() + model_kwargs = self.get_model_kwargs() + return ChatAnthropic( + model_name=model, + temperature=temperature, + model_kwargs=model_kwargs, + timeout=None, + max_retries=2, + ) +``` + +### Composing AI Assistants + +One AI Assistant can call another AI Assistant as a tool. This is useful for composing complex AI Assistants. +Use the `as_tool` method for that: + +```{.python title="myapp/ai_assistants.py" hl_lines="15 17"} +@register_assistant +class SimpleAssistant(AIAssistant): + ... + +@register_assistant +class AnotherSimpleAssistant(AIAssistant): + ... + +@register_assistant +class ComplexAssistant(AIAssistant): + ... + + def get_tools(self) -> Sequence[BaseTool]: + return [ + SimpleAssistant().as_tool( + description="Tool to <...add a meaningful description here...>"), + AnotherSimpleAssistant().as_tool( + description="Tool to <...add a meaningful description here...>"), + *super().get_tools(), + ] +``` + +The `movies/ai_assistants.py` file in the [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) +shows an example of a composed AI Assistant that's able to recommend movies and manage the user's movie backlog. + +### Retrieval Augmented Generation (RAG) + +You can use RAG in your AI Assistants. RAG means using a retriever to fetch chunks of textual data from a pre-existing DB to give +context to the LLM. This context goes into the `{context}` placeholder in the `instructions` string, namely the system prompt. +This means the LLM will have access to a context your retriever logic provides when generating the response, +thereby improving the quality of the response by avoiding generic or off-topic answers. + +For this to work, your must do the following in your AI Assistant: + +1. Add a `{context}` placeholder in the `instructions` string; +2. Add `has_rag = True` as a class attribute; +3. Override the `get_retriever` method to return a [Langchain Retriever](https://python.langchain.com/v0.2/docs/how_to/#retrievers). + +For example: + +```{.python title="myapp/ai_assistants.py" hl_lines="12 16 18"} +from django_ai_assistant import AIAssistant, register_assistant + +@register_assistant +class DocsAssistant(AIAssistant): + id = "docs_assistant" # noqa: A003 + name = "Docs Assistant" + instructions = ( + "You are an assistant for answering questions related to the provided context. " + "Use the following pieces of retrieved context to answer the user's question. " + "\n\n" + "---START OF CONTEXT---\n" + "{context}" + "---END OF CONTEXT---\n" + ) + model = "gpt-4o" + has_rag = True + + def get_retriever(self) -> BaseRetriever: + return ... # use a Langchain Retriever here +``` + +The `rag/ai_assistants.py` file in the [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) +shows an example of a RAG-powered AI Assistant that's able to answer questions about Django using the Django Documentation as context. + +### Further configuration of AI Assistants + +You can further configure the `AIAssistant` subclasses by overriding its public methods. Check the API Reference for more information. + diff --git a/example/assets/js/components/Chat/Chat.tsx b/example/assets/js/components/Chat/Chat.tsx index 60c30c7..2606c90 100644 --- a/example/assets/js/components/Chat/Chat.tsx +++ b/example/assets/js/components/Chat/Chat.tsx @@ -93,12 +93,7 @@ function ChatMessageList({ deleteMessage, }: { messages: ThreadMessagesSchemaOut[]; - deleteMessage: ({ - threadId, - messageId, - }: { - messageId: string; - }) => Promise; + deleteMessage: ({ messageId }: { messageId: string }) => Promise; }) { if (messages.length === 0) { return No messages.; diff --git a/example/example/settings.py b/example/example/settings.py index b30acb7..5116f69 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -157,7 +157,6 @@ # django-ai-assistant -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" diff --git a/example/weather/ai_assistants.py b/example/weather/ai_assistants.py index 847afbf..20bb626 100644 --- a/example/weather/ai_assistants.py +++ b/example/weather/ai_assistants.py @@ -16,6 +16,13 @@ class WeatherAIAssistant(AIAssistant): name = "Weather Assistant" model = "gpt-4o" + def get_instructions(self): + # Warning: this will use the server's timezone + # See: https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/#default-time-zone-and-current-time-zone + # In a real application, you should use the user's timezone + current_date_str = timezone.now().date().isoformat() + return f"You are a weather bot. Use the provided functions to answer questions. Today is: {current_date_str}." + @method_tool def fetch_current_weather(self, location: str) -> dict: """Fetch the current weather data for a location""" @@ -57,10 +64,3 @@ def who_am_i(self) -> str: return self._user.username else: return "Anonymous" - - def get_instructions(self): - # Warning: this will use the server's timezone - # See: https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/#default-time-zone-and-current-time-zone - # In a real application, you should use the user's timezone - current_date_str = timezone.now().date().isoformat() - return f"You are a weather bot. Use the provided functions to answer questions. Today is: {current_date_str}." diff --git a/frontend/openapi_schema.json b/frontend/openapi_schema.json index 75f5898..1adc83d 100644 --- a/frontend/openapi_schema.json +++ b/frontend/openapi_schema.json @@ -26,7 +26,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/assistants/{assistant_id}/": { @@ -55,7 +60,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/": { @@ -78,7 +88,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "post": { "operationId": "django_ai_assistant_create_thread", @@ -105,7 +120,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/": { @@ -134,7 +154,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "patch": { "operationId": "django_ai_assistant_update_thread", @@ -171,7 +196,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "delete": { "operationId": "django_ai_assistant_delete_thread", @@ -191,7 +221,12 @@ "204": { "description": "No Content" } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/messages/": { @@ -224,7 +259,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "post": { "operationId": "django_ai_assistant_create_thread_message", @@ -254,7 +294,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/messages/{message_id}/": { @@ -285,7 +330,12 @@ "204": { "description": "No Content" } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } } }, @@ -414,6 +464,13 @@ "title": "ThreadMessagesSchemaIn", "type": "object" } + }, + "securitySchemes": { + "SessionAuth": { + "type": "apiKey", + "in": "cookie", + "name": "sessionid" + } } }, "servers": [] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c0d20ec..156a272 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "cookie": "^0.6.0" }, "devDependencies": { "@hey-api/openapi-ts": "^0.46.3", "@testing-library/dom": "^10.1.0", "@testing-library/react": "^16.0.0", + "@types/cookie": "^0.6.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.1", "@types/react": "^18.3.3", @@ -2813,6 +2815,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4166,6 +4174,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 321b182..81d9036 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,8 @@ "generate-client": "openapi-ts" }, "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "cookie": "^0.6.0" }, "peerDependencies": { "react": "^18.3.1", @@ -52,6 +53,7 @@ "@hey-api/openapi-ts": "^0.46.3", "@testing-library/dom": "^10.1.0", "@testing-library/react": "^16.0.0", + "@types/cookie": "^0.6.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.1", "@types/react": "^18.3.3", diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 0437273..a82dc96 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,9 +1,14 @@ +import cookie from "cookie"; + import { OpenAPI } from "./client"; +import { AxiosRequestConfig } from "axios"; /** * Configures the base URL for the AI Assistant API which is path associated with * the Django include. * + * Configures the Axios request to include the CSRF token if it exists. + * * @param baseURL Base URL of the AI Assistant API. * * @example @@ -11,4 +16,12 @@ import { OpenAPI } from "./client"; */ export function configAIAssistant({ baseURL }: { baseURL: string }) { OpenAPI.BASE = baseURL; + + OpenAPI.interceptors.request.use((request: AxiosRequestConfig) => { + const { csrftoken } = cookie.parse(document.cookie); + if (request.headers && csrftoken) { + request.headers["X-CSRFTOKEN"] = csrftoken; + } + return request; + }); } diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..84f413e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: django-ai-assistant +site_description: Implement powerful AI assistants using Django + +repo_name: vintasoftware/django-ai-assistant +repo_url: https://github.com/vintasoftware/django-ai-assistant/ +edit_uri: blob/main/docs/ + +theme: + name: material + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +copyright: Vinta Software + +markdown_extensions: + - admonition + - pymdownx.highlight: + use_pygments: true + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets: + check_paths: true + - toc: + permalink: true + - attr_list + +nav: + - Home: index.md + - Get Started: get-started.md + - Tutorial: tutorial.md diff --git a/poetry.lock b/poetry.lock index 481369f..5466959 100644 --- a/poetry.lock +++ b/poetry.lock @@ -191,6 +191,20 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -333,6 +347,20 @@ files = [ {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" @@ -668,6 +696,23 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "gitdb" version = "4.0.11" @@ -918,6 +963,23 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +[[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 = "joblib" version = "1.4.2" @@ -956,18 +1018,18 @@ files = [ [[package]] name = "langchain" -version = "0.2.4" +version = "0.2.5" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain-0.2.4-py3-none-any.whl", hash = "sha256:a04813215c30f944df006031e2febde872af8fab628dcee825d969e07b6cd621"}, - {file = "langchain-0.2.4.tar.gz", hash = "sha256:e704b5b06222d5eba2d02c76f891321d1bac8952ed54e093831b2bdabf99dcd5"}, + {file = "langchain-0.2.5-py3-none-any.whl", hash = "sha256:9aded9a65348254e1c93dcdaacffe4d1b6a5e7f74ef80c160c88ff78ad299228"}, + {file = "langchain-0.2.5.tar.gz", hash = "sha256:ffdbf4fcea46a10d461bcbda2402220fcfd72a0c70e9f4161ae0510067b9b3bd"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" -langchain-core = ">=0.2.6,<0.3.0" +langchain-core = ">=0.2.7,<0.3.0" langchain-text-splitters = ">=0.2.0,<0.3.0" langsmith = ">=0.1.17,<0.2.0" numpy = {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""} @@ -979,22 +1041,22 @@ tenacity = ">=8.1.0,<9.0.0" [[package]] name = "langchain-community" -version = "0.2.4" +version = "0.2.5" description = "Community contributed LangChain integrations." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_community-0.2.4-py3-none-any.whl", hash = "sha256:8582e9800f4837660dc297cccd2ee1ddc1d8c440d0fe8b64edb07620f0373b0e"}, - {file = "langchain_community-0.2.4.tar.gz", hash = "sha256:2bb6a1a36b8500a564d25d76469c02457b1a7c3afea6d4a609a47c06b993e3e4"}, + {file = "langchain_community-0.2.5-py3-none-any.whl", hash = "sha256:bf37a334952e42c7676d083cf2d2c4cbfbb7de1949c4149fe19913e2b06c485f"}, + {file = "langchain_community-0.2.5.tar.gz", hash = "sha256:476787b8c8c213b67e7b0eceb53346e787f00fbae12d8e680985bd4f93b0bf64"}, ] [package.dependencies] aiohttp = ">=3.8.3,<4.0.0" dataclasses-json = ">=0.5.7,<0.7" -langchain = ">=0.2.0,<0.3.0" -langchain-core = ">=0.2.0,<0.3.0" +langchain = ">=0.2.5,<0.3.0" +langchain-core = ">=0.2.7,<0.3.0" langsmith = ">=0.1.0,<0.2.0" -numpy = ">=1,<2" +numpy = {version = ">=1.26.0,<2.0.0", markers = "python_version >= \"3.12\""} PyYAML = ">=5.3" requests = ">=2,<3" SQLAlchemy = ">=1.4,<3" @@ -1002,22 +1064,25 @@ tenacity = ">=8.1.0,<9.0.0" [[package]] name = "langchain-core" -version = "0.2.6" +version = "0.2.9" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.2.6-py3-none-any.whl", hash = "sha256:90521c9fc95d8f925e0d2e2d952382676aea6d3f8de611eda1b1810874c31e5d"}, - {file = "langchain_core-0.2.6.tar.gz", hash = "sha256:9f0e38da722a558a6e95b6d86de01bd92e84558c47ac8ba599f02eab70a1c873"}, + {file = "langchain_core-0.2.9-py3-none-any.whl", hash = "sha256:426a5a4fea95a5db995ba5ab560b76edd4998fb6fe52ccc28ac987092a4cbfcd"}, + {file = "langchain_core-0.2.9.tar.gz", hash = "sha256:f1c59082642921727844e1cd0eb36d451edd1872c20e193aa3142aac03495986"}, ] [package.dependencies] jsonpatch = ">=1.33,<2.0" langsmith = ">=0.1.75,<0.2.0" packaging = ">=23.2,<25" -pydantic = ">=1,<3" +pydantic = [ + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, + {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, +] PyYAML = ">=5.3" -tenacity = ">=8.1.0,<9.0.0" +tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langchain-openai" @@ -1054,13 +1119,13 @@ extended-testing = ["beautifulsoup4 (>=4.12.3,<5.0.0)", "lxml (>=4.9.3,<6.0)"] [[package]] name = "langsmith" -version = "0.1.77" +version = "0.1.80" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langsmith-0.1.77-py3-none-any.whl", hash = "sha256:2202cc21b1ed7e7b9e5d2af2694be28898afa048c09fdf09f620cbd9301755ae"}, - {file = "langsmith-0.1.77.tar.gz", hash = "sha256:4ace09077a9a4e412afeb4b517ca68e7de7b07f36e4792dc8236ac5207c0c0c7"}, + {file = "langsmith-0.1.80-py3-none-any.whl", hash = "sha256:951fc29576b52afd8378d41f6db343090fea863e3620f0ca97e83b221f93c94d"}, + {file = "langsmith-0.1.80.tar.gz", hash = "sha256:a29b1dde27612308beee424f1388ad844c8e7e375bf2ac8bdf4da174013f279d"}, ] [package.dependencies] @@ -1226,6 +1291,90 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.10)"] +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[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 = "marshmallow" version = "3.21.3" @@ -1259,6 +1408,103 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.0" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.27" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"}, + {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + [[package]] name = "model-bakery" version = "1.18.1" @@ -1532,6 +1778,16 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "parso" version = "0.8.4" @@ -1547,6 +1803,17 @@ files = [ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["docopt", "pytest"] +[[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 = "pexpect" version = "4.9.0" @@ -1773,6 +2040,24 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pytest" version = "8.2.2" @@ -1866,6 +2151,20 @@ vcrpy = ">=2.0.1" dev = ["pytest-recording[tests]"] tests = ["pytest-httpbin", "pytest-mock", "requests", "werkzeug (==3.0.1)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1940,6 +2239,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "regex" version = "2024.5.15" @@ -2208,64 +2521,64 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.30" +version = "2.0.31" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8"}, - {file = "SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:955991a09f0992c68a499791a753523f50f71a6885531568404fa0f231832aa0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f69e4c756ee2686767eb80f94c0125c8b0a0b87ede03eacc5c8ae3b54b99dc46"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c9db1ce00e59e8dd09d7bae852a9add716efdc070a3e2068377e6ff0d6fdaa"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1429a4b0f709f19ff3b0cf13675b2b9bfa8a7e79990003207a011c0db880a13"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:efedba7e13aa9a6c8407c48facfdfa108a5a4128e35f4c68f20c3407e4376aa9"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16863e2b132b761891d6c49f0a0f70030e0bcac4fd208117f6b7e053e68668d0"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win32.whl", hash = "sha256:2ecabd9ccaa6e914e3dbb2aa46b76dede7eadc8cbf1b8083c94d936bcd5ffb49"}, - {file = "SQLAlchemy-2.0.30-cp311-cp311-win_amd64.whl", hash = "sha256:0b3f4c438e37d22b83e640f825ef0f37b95db9aa2d68203f2c9549375d0b2260"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a79d65395ac5e6b0c2890935bad892eabb911c4aa8e8015067ddb37eea3d56c"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a5baf9267b752390252889f0c802ea13b52dfee5e369527da229189b8bd592e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5a646930c5123f8461f6468901573f334c2c63c795b9af350063a736d0134"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296230899df0b77dec4eb799bcea6fbe39a43707ce7bb166519c97b583cfcab3"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c62d401223f468eb4da32627bffc0c78ed516b03bb8a34a58be54d618b74d472"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3b69e934f0f2b677ec111b4d83f92dc1a3210a779f69bf905273192cf4ed433e"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win32.whl", hash = "sha256:77d2edb1f54aff37e3318f611637171e8ec71472f1fdc7348b41dcb226f93d90"}, - {file = "SQLAlchemy-2.0.30-cp312-cp312-win_amd64.whl", hash = "sha256:b6c7ec2b1f4969fc19b65b7059ed00497e25f54069407a8701091beb69e591a5"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a8e3b0a7e09e94be7510d1661339d6b52daf202ed2f5b1f9f48ea34ee6f2d57"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b60203c63e8f984df92035610c5fb76d941254cf5d19751faab7d33b21e5ddc0"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1dc3eabd8c0232ee8387fbe03e0a62220a6f089e278b1f0aaf5e2d6210741ad"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:40ad017c672c00b9b663fcfcd5f0864a0a97828e2ee7ab0c140dc84058d194cf"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e42203d8d20dc704604862977b1470a122e4892791fe3ed165f041e4bf447a1b"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win32.whl", hash = "sha256:2a4f4da89c74435f2bc61878cd08f3646b699e7d2eba97144030d1be44e27584"}, - {file = "SQLAlchemy-2.0.30-cp37-cp37m-win_amd64.whl", hash = "sha256:b6bf767d14b77f6a18b6982cbbf29d71bede087edae495d11ab358280f304d8e"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc0c53579650a891f9b83fa3cecd4e00218e071d0ba00c4890f5be0c34887ed3"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:311710f9a2ee235f1403537b10c7687214bb1f2b9ebb52702c5aa4a77f0b3af7"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:408f8b0e2c04677e9c93f40eef3ab22f550fecb3011b187f66a096395ff3d9fd"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a4b4fb0dd4d2669070fb05b8b8824afd0af57587393015baee1cf9890242d9"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a943d297126c9230719c27fcbbeab57ecd5d15b0bd6bfd26e91bfcfe64220621"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a089e218654e740a41388893e090d2e2c22c29028c9d1353feb38638820bbeb"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win32.whl", hash = "sha256:fa561138a64f949f3e889eb9ab8c58e1504ab351d6cf55259dc4c248eaa19da6"}, - {file = "SQLAlchemy-2.0.30-cp38-cp38-win_amd64.whl", hash = "sha256:7d74336c65705b986d12a7e337ba27ab2b9d819993851b140efdf029248e818e"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8c62fe2480dd61c532ccafdbce9b29dacc126fe8be0d9a927ca3e699b9491a"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2383146973a15435e4717f94c7509982770e3e54974c71f76500a0136f22810b"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8409de825f2c3b62ab15788635ccaec0c881c3f12a8af2b12ae4910a0a9aeef6"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0094c5dc698a5f78d3d1539853e8ecec02516b62b8223c970c86d44e7a80f6c7"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:edc16a50f5e1b7a06a2dcc1f2205b0b961074c123ed17ebda726f376a5ab0953"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f7703c2010355dd28f53deb644a05fc30f796bd8598b43f0ba678878780b6e4c"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win32.whl", hash = "sha256:1f9a727312ff6ad5248a4367358e2cf7e625e98b1028b1d7ab7b806b7d757513"}, - {file = "SQLAlchemy-2.0.30-cp39-cp39-win_amd64.whl", hash = "sha256:a0ef36b28534f2a5771191be6edb44cc2673c7b2edf6deac6562400288664221"}, - {file = "SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a"}, - {file = "SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, + {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"}, + {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"}, + {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"}, + {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"}, + {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, + {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, + {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, + {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -2329,13 +2642,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "tenacity" -version = "8.3.0" +version = "8.4.1" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" files = [ - {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"}, - {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"}, + {file = "tenacity-8.4.1-py3-none-any.whl", hash = "sha256:28522e692eda3e1b8f5e99c51464efcc0b9fc86933da92415168bc1c4e2308fa"}, + {file = "tenacity-8.4.1.tar.gz", hash = "sha256:54b1412b878ddf7e1f1577cd49527bad8cdef32421bd599beac0c6c3f10582fd"}, ] [package.extras] @@ -2479,13 +2792,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.18" +version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, - {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, ] [package.extras] @@ -2532,6 +2845,50 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[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)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -2742,4 +3099,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12,<3.13" -content-hash = "a691791a02fbae2f75b9854b4e4da4e81a29aba8589be5e66269eac19e339c61" +content-hash = "138a368288a03b6c2d1a9a71a148bdfd7a037bfdf93995144c47b5e228d0aa5f" diff --git a/pyproject.toml b/pyproject.toml index f0117ee..b4e58e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,11 @@ ipython = "^8.24.0" pytest-asyncio = "^0.23.7" pytest-recording = "^0.13.1" coveralls = "^4.0.1" +mkdocs = "^1.6.0" +mkdocs-material = "^9.5.27" +pymdown-extensions = "^10.8.1" +markdown = "^3.6" +pygments = "^2.18.0" model-bakery = "^1.18.1" pytest-cov = "^5.0.0" @@ -70,7 +75,7 @@ select = [ # flake8-no-pep420 "INP", # Ruff-specific rules - "RUF" + "RUF", ] exclude = [ ".bzr", @@ -98,33 +103,33 @@ exclude = [ "*/migrations/*", ] ignore = [ - # Disable eradicate (commented code removal) - "ERA001", - # Disable Conflicting lint rules, - # see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules - "W191", - "E501", - "E111", - "E117", - "D206", - "D300", - "Q000", - "Q001", - "Q002", - "Q003", - "COM812", - "COM819", - "ISC001", - "ISC002", - # Allow `except Exception`: - "BLE001", - # Disable unused `noqa` directive - "RUF100", - # Disable pyupgrade UP rules that conflict with django-ninja - "UP006", - "UP035", - "UP037", - "UP040", + # Disable eradicate (commented code removal) + "ERA001", + # Disable Conflicting lint rules, + # see https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E501", + "E111", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", + # Allow `except Exception`: + "BLE001", + # Disable unused `noqa` directive + "RUF100", + # Disable pyupgrade UP rules that conflict with django-ninja + "UP006", + "UP035", + "UP037", + "UP040", ] line-length = 100 indent-width = 4 @@ -136,7 +141,14 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" ignore-overlong-task-comments = true [tool.ruff.lint.isort] -section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"] +section-order = [ + "future", + "standard-library", + "django", + "third-party", + "first-party", + "local-folder", +] lines-after-imports = 2 [tool.ruff.lint.isort.sections] @@ -156,7 +168,15 @@ lines-after-imports = 2 [tool.coverage.run] branch = true source = ["backend"] -omit = ["**/venv/*", "**/env/*", "**/virtualenvs/*", "**/node_modules/*", "**/migrations/*", "**/settings/*", "**/tests/*"] +omit = [ + "**/venv/*", + "**/env/*", + "**/virtualenvs/*", + "**/node_modules/*", + "**/migrations/*", + "**/settings/*", + "**/tests/*", +] [tool.pyright] pythonVersion = "3.12" diff --git a/tests/settings.py b/tests/settings.py index 82ddef4..cef44a6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -106,8 +106,7 @@ # django-ai-assistant -# Comment the OPENAI_API_KEY below and set one on .env.tests file at root when updating the VCRs: -OPENAI_API_KEY = "sk-fake-test-key-123" +# NOTE: set a OPENAI_API_KEY on .env.tests file at root when updating the VCRs. AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" diff --git a/tests/test_views.py b/tests/test_views.py index ae04c7d..58d4b99 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -51,8 +51,9 @@ def authenticated_client(client): # Assistant Views -def test_list_assistants_with_results(client): - response = client.get(reverse("django_ai_assistant:assistants_list")) +@pytest.mark.django_db() +def test_list_assistants_with_results(authenticated_client): + response = authenticated_client.get(reverse("django_ai_assistant:assistants_list")) assert response.status_code == HTTPStatus.OK assert response.json() == [{"id": "temperature_assistant", "name": "Temperature Assistant"}] @@ -63,8 +64,9 @@ def test_does_not_list_assistants_if_unauthorized(): pass -def test_get_assistant_that_exists(client): - response = client.get( +@pytest.mark.django_db() +def test_get_assistant_that_exists(authenticated_client): + response = authenticated_client.get( reverse( "django_ai_assistant:assistant_detail", kwargs={"assistant_id": "temperature_assistant"} ) @@ -74,9 +76,10 @@ def test_get_assistant_that_exists(client): assert response.json() == {"id": "temperature_assistant", "name": "Temperature Assistant"} -def test_get_assistant_that_does_not_exist(client): +@pytest.mark.django_db() +def test_get_assistant_that_does_not_exist(authenticated_client): with pytest.raises(AIAssistantNotDefinedError): - client.get( + authenticated_client.get( reverse( "django_ai_assistant:assistant_detail", kwargs={"assistant_id": "fake_assistant"} )