From 3c81b0a7cba60828074036e73991b253645289d9 Mon Sep 17 00:00:00 2001 From: tim_curchod Date: Wed, 18 Sep 2024 18:57:08 +0900 Subject: [PATCH] #2 created the polls api --- docs/work/2024.md | 309 +++++++++++++++++++++++++++++++ drf_two/settings.py | 1 + drf_two/urls.py | 1 + polls/__init__.py | 0 polls/admin.py | 3 + polls/apps.py | 6 + polls/migrations/0001_initial.py | 55 ++++++ polls/migrations/__init__.py | 0 polls/models.py | 61 ++++++ polls/serializers.py | 79 ++++++++ polls/tests.py | 3 + polls/urls.py | 16 ++ polls/views.py | 94 ++++++++++ requirements.txt | 136 +++++++++++++- 14 files changed, 762 insertions(+), 2 deletions(-) create mode 100644 polls/__init__.py create mode 100644 polls/admin.py create mode 100644 polls/apps.py create mode 100644 polls/migrations/0001_initial.py create mode 100644 polls/migrations/__init__.py create mode 100644 polls/models.py create mode 100644 polls/serializers.py create mode 100644 polls/tests.py create mode 100644 polls/urls.py create mode 100644 polls/views.py diff --git a/docs/work/2024.md b/docs/work/2024.md index f55d380..adcfd66 100644 --- a/docs/work/2024.md +++ b/docs/work/2024.md @@ -1,5 +1,314 @@ # 2024 +## September + +### Polls + +After a while, I need to refresh a few things. + +First merge master into dev to update the dev branch after a flurry of work to solve some issues. + +Next, run the app locally. + +```txt +System check identified no issues (0 silenced). +You have 34 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): account, admin, auth, authtoken, comments, contenttypes, followers, likes, posts, profiles, sessions, sites, socialaccount. +Run 'python manage.py migrate' to apply them. +September 18, 2024 - 17:15:39 +``` + +Ran python manage.py migrate and started the server and it works as expected. + +#### Step 1 + +Next, scaffold the polls app. + +```sh +python manage.py startapp polls +``` + +#### Step 2 + +Add it to the installed apps array in drf_two\settings.py. + +Register the Polls model in admin.py + +```py +from django.contrib import admin + +from .models import Polls + +admin.site.register(Polls) +``` + +#### Step 3 + +Then run make migrations which you have to do after updating a model: + +```shell +python manage.py makemigrations +... + from .models import Polls +ImportError: cannot import name 'Polls' from 'polls.models' (C:\Users\timof\repos\timo\drf-two\polls\models.py) +``` + +Actually, our guide has no admin.py changes, so roll those back and: + +#### Step 4 + +Migrate the changes. + +```py +$ python manage.py makemigrations +Migrations for 'polls': + polls\migrations\0001_initial.py + - Create model Question + - Create model Answer + - Create model Vote +``` + +And python manage.py migrate: + +```py +$ python manage.py migrate +Operations to perform: + Apply all migrations: account, admin, auth, authtoken, comments, contenttypes, followers, likes, polls, posts, profiles, sessions, sites, socialaccount +Running migrations: + Applying polls.0001_initial... OK +``` + +Run the server: + +```shell +python manage.py runserver +``` + +Goto the admin url: http://127.0.0.1:8000/admin + +Create a file with the dependencies: + +```shell +pip freeze > requirements.txt +``` + +#### Step 5 + +Create the serializers.py file. + +```py +from rest_framework import serializers +from .models import Question, Answer, Vote + + +class VoteSerializer(serializers.ModelSerializer): + """ + Serializer for the Vote model. It handles serialization for creating and + listing votes. Ensures that a user cannot vote more than once per question + by implementing custom validation. + """ + class Meta: + model = Vote + fields = ['id', 'answer', 'voter', 'created_at'] + read_only_fields = ('voter',) + + def validate(self, data): + """ + Validate that the user has not already voted for the same question. + Raises a ValidationError if the user has already voted. + """ + question = data['answer'].question + voter = self.context['request'].user + if Vote.objects.filter(answer__question=question, + voter=voter).exists(): + raise serializers.ValidationError( + "You have already voted on this question.") + return data + + def save(self, **kwargs): + kwargs['voter'] = self.context['request'].user + return super().save(**kwargs) + + +class AnswerSerializer(serializers.ModelSerializer): + """ + Serializer for the Answer model. Includes a custom method to count votes, + which is included in the serialization output. + """ + text = serializers.CharField(required=True, allow_blank=False, error_messages={"blank": "This field may not be left blank."}) + votes_count = serializers.SerializerMethodField() + + def get_votes_count(self, obj): + return obj.votes.distinct().count() + + class Meta: + model = Answer + fields = ['id', 'text', 'created_at', 'votes_count'] + + +class QuestionSerializer(serializers.ModelSerializer): + """ + Serializer for the Question model. Handles serialization for creating + and listing questions. Includes nested AnswerSerializers to represent + answers associated with the question, and custom methods to handle + the creation of answers within the same request as a question. + """ + owner = serializers.PrimaryKeyRelatedField(read_only=True) + owner_username = serializers.ReadOnlyField(source='owner.username') + votes_count = serializers.IntegerField(read_only=True) + answers = AnswerSerializer(many=True, required=True) + + def validate_answers(self, value): + if len(value) < 2: + raise serializers.ValidationError("At least two answers are required.") + return value + + class Meta: + model = Question + fields = [ + 'id', 'owner', 'owner_username', 'text', + 'created_at', 'answers', 'votes_count' + ] + + def create(self, validated_data): + answers_data = validated_data.pop('answers', []) + question = Question.objects.create(**validated_data) + for answer_data in answers_data: + Answer.objects.create(question=question, **answer_data) + return question +``` + +#### Step 6 + +Add: Urls in both polls and main urls and add views in polls. + +In polls/urls.py: + +```py +from django.urls import path +from . import views + +urlpatterns = [ + # Questions + path('questions/', views.QuestionList.as_view(), name='question-list'), + path('questions//', views.QuestionDetail.as_view(), name='question-detail'), + + # Answers + path('answers/', views.AnswerList.as_view(), name='answer-list'), + path('answers//', views.AnswerDetail.as_view(), name='answer-detail'), + + # Votes + path('votes/', views.VoteList.as_view(), name='vote-list'), + path('votes//', views.VoteDetail.as_view(), name='vote-detail'), +] +``` + +In drf_two\urls.py: + +```py +path('', include('polls.urls')), +``` + +Update the polls\views.py: + +```py +from django.db.models import Count +from rest_framework import generics, permissions, filters +from rest_framework.exceptions import ValidationError +from .models import Question, Answer, Vote +from .serializers import QuestionSerializer, AnswerSerializer, VoteSerializer +from drf_two.permissions import IsOwnerOrReadOnly + + +class QuestionList(generics.ListCreateAPIView): + """ + Provides a list of all questions and allows authenticated users to create + new questions. Questions are listed with a count of votes for their + answers, ordered by creation date in descending order. The view also + supports search functionality on question text and owner username. + """ + queryset = Question.objects.annotate(votes_count=Count( + 'answers__votes', distinct=True)).order_by('-created_at') + serializer_class = QuestionSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter] + search_fields = [ + 'owner__username', + 'text', + ] + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + +class QuestionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Provides detailed view for a specific question, including capabilities + to update or delete. Only the owner of the question has the permissions + to update or delete it. + """ + queryset = Question.objects.annotate(votes_count=Count( + 'answers__votes', distinct=True)).order_by('-created_at') + serializer_class = QuestionSerializer + permission_classes = [IsOwnerOrReadOnly] + + +class AnswerList(generics.ListCreateAPIView): + """ + Lists all answers for questions, annotated with a count of votes + for each answer, ordered by their creation date in descending order. + """ + queryset = Answer.objects.annotate(votes_count=Count( + 'votes', distinct=True)).order_by('-created_at') + serializer_class = AnswerSerializer + + +class AnswerDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Provides a detailed view for a specific answer, allowing retrieval, + update, and deletion operations. Updates and deletions are restricted + to the owner of the answer. + """ + queryset = Answer.objects.annotate(votes_count=Count( + 'votes', distinct=True)) + serializer_class = AnswerSerializer + + +class VoteList(generics.ListCreateAPIView): + """ + Provides a list of all votes and allows authenticated users + to create a vote on an answer. Prevents a user from voting + more than once on the same question. + """ + queryset = Vote.objects.all() + serializer_class = VoteSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def perform_create(self, serializer): + user = self.request.user + question = serializer.validated_data['answer'].question + + if Vote.objects.filter(answer__question=question, voter=user).exists(): + raise ValidationError( + {"error": "You have already voted on this question"}) + + serializer.save(voter=user, question=question) + + def get_serializer_context(self): + context = super().get_serializer_context() + context['request'] = self.request + return context + + +class VoteDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Allows detailed operations on a specific vote. + """ + queryset = Vote.objects.all() + serializer_class = VoteSerializer +``` + +## March + It looks like we should not be committing the db.sqlite3 file so adding that to the gitignore file. ```sh diff --git a/drf_two/settings.py b/drf_two/settings.py index d83ba40..9db3f33 100644 --- a/drf_two/settings.py +++ b/drf_two/settings.py @@ -96,6 +96,7 @@ 'comments', 'likes', 'followers', + 'polls', ] SITE_ID = 1 MIDDLEWARE = [ diff --git a/drf_two/urls.py b/drf_two/urls.py index e6d26bf..40b6a41 100644 --- a/drf_two/urls.py +++ b/drf_two/urls.py @@ -30,4 +30,5 @@ path('', include('comments.urls')), path('', include('likes.urls')), path('', include('followers.urls')), + path('', include('polls.urls')), ] \ No newline at end of file diff --git a/polls/__init__.py b/polls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polls/admin.py b/polls/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/polls/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/polls/apps.py b/polls/apps.py new file mode 100644 index 0000000..5a5f94c --- /dev/null +++ b/polls/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PollsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'polls' diff --git a/polls/migrations/0001_initial.py b/polls/migrations/0001_initial.py new file mode 100644 index 0000000..ef0a865 --- /dev/null +++ b/polls/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2 on 2024-09-18 08:36 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Answer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='polls.question')), + ], + options={ + 'ordering': ['created_at'], + }, + ), + migrations.CreateModel( + name='Vote', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='polls.answer')), + ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + 'unique_together': {('question', 'voter')}, + }, + ), + ] diff --git a/polls/migrations/__init__.py b/polls/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polls/models.py b/polls/models.py new file mode 100644 index 0000000..4a425da --- /dev/null +++ b/polls/models.py @@ -0,0 +1,61 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Question(models.Model): + """ + Represents a question posted by a user. A question is identified + by its owner and the textual content of the question itself. Each + question has a timestamp indicating when it was created and + can be associated with multiple answers. + """ + owner = models.ForeignKey( + User, on_delete=models.CASCADE, related_name='questions') + text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return f"Question {self.id} by {self.owner}" + + +class Answer(models.Model): + """ + Represents an answer to a specific question. Each answer is linked + to a question and contains text as its content. Answers are + timestamped at creation. + """ + question = models.ForeignKey( + Question, on_delete=models.CASCADE, related_name='answers') + text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['created_at'] + + def __str__(self): + return f"Answer {self.id} to Question {self.question.id}" + + +class Vote(models.Model): + """ + Represents a vote cast by a user for an answer to a question. Each vote is + linked not only to the answer but also directly to the question for + integrity and quick querying purposes. A user can only vote once per + question, ensuring uniqueness of the voter-question pair. + """ + answer = models.ForeignKey( + Answer, on_delete=models.CASCADE, related_name='votes') + voter = models.ForeignKey( + User, on_delete=models.CASCADE) + question = models.ForeignKey(Question, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('question', 'voter') + ordering = ['-created_at'] + + def __str__(self): + return f"Vote by {self.voter} for {self.answer}" \ No newline at end of file diff --git a/polls/serializers.py b/polls/serializers.py new file mode 100644 index 0000000..14976b5 --- /dev/null +++ b/polls/serializers.py @@ -0,0 +1,79 @@ +from rest_framework import serializers +from .models import Question, Answer, Vote + + +class VoteSerializer(serializers.ModelSerializer): + """ + Serializer for the Vote model. It handles serialization for creating and + listing votes. Ensures that a user cannot vote more than once per question + by implementing custom validation. + """ + class Meta: + model = Vote + fields = ['id', 'answer', 'voter', 'created_at'] + read_only_fields = ('voter',) + + def validate(self, data): + """ + Validate that the user has not already voted for the same question. + Raises a ValidationError if the user has already voted. + """ + question = data['answer'].question + voter = self.context['request'].user + if Vote.objects.filter(answer__question=question, + voter=voter).exists(): + raise serializers.ValidationError( + "You have already voted on this question.") + return data + + def save(self, **kwargs): + kwargs['voter'] = self.context['request'].user + return super().save(**kwargs) + + +class AnswerSerializer(serializers.ModelSerializer): + """ + Serializer for the Answer model. Includes a custom method to count votes, + which is included in the serialization output. + """ + text = serializers.CharField(required=True, allow_blank=False, error_messages={"blank": "This field may not be left blank."}) + votes_count = serializers.SerializerMethodField() + + def get_votes_count(self, obj): + return obj.votes.distinct().count() + + class Meta: + model = Answer + fields = ['id', 'text', 'created_at', 'votes_count'] + + +class QuestionSerializer(serializers.ModelSerializer): + """ + Serializer for the Question model. Handles serialization for creating + and listing questions. Includes nested AnswerSerializers to represent + answers associated with the question, and custom methods to handle + the creation of answers within the same request as a question. + """ + owner = serializers.PrimaryKeyRelatedField(read_only=True) + owner_username = serializers.ReadOnlyField(source='owner.username') + votes_count = serializers.IntegerField(read_only=True) + answers = AnswerSerializer(many=True, required=True) + + def validate_answers(self, value): + if len(value) < 2: + raise serializers.ValidationError("At least two answers are required.") + return value + + class Meta: + model = Question + fields = [ + 'id', 'owner', 'owner_username', 'text', + 'created_at', 'answers', 'votes_count' + ] + + def create(self, validated_data): + answers_data = validated_data.pop('answers', []) + question = Question.objects.create(**validated_data) + for answer_data in answers_data: + Answer.objects.create(question=question, **answer_data) + return question diff --git a/polls/tests.py b/polls/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/polls/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/polls/urls.py b/polls/urls.py new file mode 100644 index 0000000..8b048fe --- /dev/null +++ b/polls/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from . import views + +urlpatterns = [ + # Questions + path('questions/', views.QuestionList.as_view(), name='question-list'), + path('questions//', views.QuestionDetail.as_view(), name='question-detail'), + + # Answers + path('answers/', views.AnswerList.as_view(), name='answer-list'), + path('answers//', views.AnswerDetail.as_view(), name='answer-detail'), + + # Votes + path('votes/', views.VoteList.as_view(), name='vote-list'), + path('votes//', views.VoteDetail.as_view(), name='vote-detail'), +] \ No newline at end of file diff --git a/polls/views.py b/polls/views.py new file mode 100644 index 0000000..3578931 --- /dev/null +++ b/polls/views.py @@ -0,0 +1,94 @@ +from django.db.models import Count +from rest_framework import generics, permissions, filters +from rest_framework.exceptions import ValidationError +from .models import Question, Answer, Vote +from .serializers import QuestionSerializer, AnswerSerializer, VoteSerializer +from drf_two.permissions import IsOwnerOrReadOnly + + +class QuestionList(generics.ListCreateAPIView): + """ + Provides a list of all questions and allows authenticated users to create + new questions. Questions are listed with a count of votes for their + answers, ordered by creation date in descending order. The view also + supports search functionality on question text and owner username. + """ + queryset = Question.objects.annotate(votes_count=Count( + 'answers__votes', distinct=True)).order_by('-created_at') + serializer_class = QuestionSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter] + search_fields = [ + 'owner__username', + 'text', + ] + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + +class QuestionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Provides detailed view for a specific question, including capabilities + to update or delete. Only the owner of the question has the permissions + to update or delete it. + """ + queryset = Question.objects.annotate(votes_count=Count( + 'answers__votes', distinct=True)).order_by('-created_at') + serializer_class = QuestionSerializer + permission_classes = [IsOwnerOrReadOnly] + + +class AnswerList(generics.ListCreateAPIView): + """ + Lists all answers for questions, annotated with a count of votes + for each answer, ordered by their creation date in descending order. + """ + queryset = Answer.objects.annotate(votes_count=Count( + 'votes', distinct=True)).order_by('-created_at') + serializer_class = AnswerSerializer + + +class AnswerDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Provides a detailed view for a specific answer, allowing retrieval, + update, and deletion operations. Updates and deletions are restricted + to the owner of the answer. + """ + queryset = Answer.objects.annotate(votes_count=Count( + 'votes', distinct=True)) + serializer_class = AnswerSerializer + + +class VoteList(generics.ListCreateAPIView): + """ + Provides a list of all votes and allows authenticated users + to create a vote on an answer. Prevents a user from voting + more than once on the same question. + """ + queryset = Vote.objects.all() + serializer_class = VoteSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def perform_create(self, serializer): + user = self.request.user + question = serializer.validated_data['answer'].question + + if Vote.objects.filter(answer__question=question, voter=user).exists(): + raise ValidationError( + {"error": "You have already voted on this question"}) + + serializer.save(voter=user, question=question) + + def get_serializer_context(self): + context = super().get_serializer_context() + context['request'] = self.request + return context + + +class VoteDetail(generics.RetrieveUpdateDestroyAPIView): + """ + Allows detailed operations on a specific vote. + """ + queryset = Vote.objects.all() + serializer_class = VoteSerializer diff --git a/requirements.txt b/requirements.txt index f16e235..6dcaba3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,31 @@ +accelerate==0.24.1 +aiohttp==3.9.0 +aiosignal==1.3.1 +altair==5.1.2 +annotated-types==0.6.0 +anyio==3.7.1 asgiref==3.7.2 +asttokens==2.4.1 +async-timeout==4.0.3 +attrs==23.1.0 +blinker==1.7.0 +cachetools==5.3.2 certifi==2023.11.17 cffi==1.16.0 charset-normalizer==3.3.2 +click==8.1.7 cloudinary==1.36.0 +colorama==0.4.6 +comm==0.2.2 +contourpy==1.2.0 cryptography==41.0.7 +ctransformers==0.2.27 +cycler==0.12.1 +dataclasses-json==0.6.2 +debugpy==1.8.1 +decorator==5.1.1 defusedxml==0.7.1 +distro==1.8.0 dj-database-url==0.5.0 dj-rest-auth==2.1.9 Django==3.2 @@ -14,19 +35,130 @@ django-cors-headers==4.3.1 django-filter==23.5 djangorestframework==3.14.0 djangorestframework-simplejwt==5.3.0 +et-xmlfile==1.1.0 +exceptiongroup==1.2.0 +executing==2.0.1 +faiss-cpu==1.7.4 +fastapi==0.110.0 +filelock==3.13.1 +fonttools==4.50.0 +frozenlist==1.4.0 +fsspec==2023.10.0 +gitdb==4.0.11 +GitPython==3.1.40 +google-auth==2.32.0 +google-auth-oauthlib==1.2.1 +greenlet==3.0.1 +gspread==6.1.2 gunicorn==21.2.0 -idna==3.6 +h11==0.14.0 +httpcore==1.0.2 +httptools==0.6.1 +httpx==0.25.1 +huggingface==0.0.1 +huggingface-hub==0.19.4 +idna==3.4 +importlib-metadata==6.8.0 +ipykernel==6.29.3 +ipython==8.22.2 +jedi==0.19.1 +Jinja2==3.1.2 +joblib==1.3.2 +jsonpatch==1.33 +jsonpointer==2.4 +jsonschema==4.20.0 +jsonschema-specifications==2023.11.1 +jupyter_client==8.6.1 +jupyter_core==5.7.2 +kiwisolver==1.4.5 +langchain==0.0.339 +langsmith==0.0.65 +markdown-it-py==3.0.0 +MarkupSafe==2.1.3 +marshmallow==3.20.1 +matplotlib==3.8.3 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +mpmath==1.3.0 +multidict==6.0.4 +mypy-extensions==1.0.0 +nest-asyncio==1.6.0 +networkx==3.2.1 +nltk==3.8.1 +numpy==1.26.2 oauthlib==3.2.2 +openai==1.3.5 +openpyxl==3.1.2 packaging==23.2 +pandas==2.1.3 +parso==0.8.3 Pillow==10.1.0 -psycopg2==2.9.9 +platformdirs==4.2.0 +prompt-toolkit==3.0.43 +protobuf==4.25.1 +psutil==5.9.6 +pure-eval==0.2.2 +py-cpuinfo==9.0.0 +pyarrow==14.0.1 +pyasn1==0.6.0 +pyasn1_modules==0.4.0 pycparser==2.21 +pydantic==2.5.1 +pydantic_core==2.14.3 +pydeck==0.8.1b0 +Pygments==2.17.1 PyJWT==2.8.0 +pyparsing==3.1.2 +PyPDF2==3.0.1 +python-dateutil==2.8.2 +python-dotenv==1.0.0 python3-openid==3.2.0 pytz==2023.3.post1 +pywin32==306 +PyYAML==6.0.1 +pyzmq==25.1.2 +referencing==0.31.0 +regex==2023.10.3 requests==2.31.0 requests-oauthlib==1.3.1 +rich==13.7.0 +rpds-py==0.13.1 +rsa==4.9 +safetensors==0.4.0 +scikit-learn==1.3.2 +scipy==1.11.4 +sentence-transformers==2.2.2 +sentencepiece==0.1.99 six==1.16.0 +smmap==5.0.1 +sniffio==1.3.0 +SQLAlchemy==2.0.23 sqlparse==0.4.4 +stack-data==0.6.3 +starlette==0.36.3 +streamlit==1.28.2 +sympy==1.12 +tenacity==8.2.3 +threadpoolctl==3.2.0 +tokenizers==0.15.0 +toml==0.10.2 +toolz==0.12.0 +torch==2.1.1 +torchvision==0.16.1 +tornado==6.3.3 +tqdm==4.66.1 +traitlets==5.14.2 +transformers==4.35.2 +typing-inspect==0.9.0 typing_extensions==4.8.0 +tzdata==2023.3 +tzlocal==5.2 urllib3==2.1.0 +uvicorn==0.29.0 +validators==0.22.0 +watchdog==3.0.0 +watchfiles==0.21.0 +wcwidth==0.2.13 +websockets==12.0 +yarl==1.9.2 +zipp==3.17.0